Coverage for src/data_api/domain/energy/services.py: 100%

35 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-13 19:28 +0000

1""" 

2Energy domain service. 

3 

4Handles real-time electricity usage data fetching. 

5""" 

6 

7import asyncio 

8import datetime 

9import re 

10 

11import httpx 

12 

13from data_api.core.exceptions import UpstreamResponseException 

14from data_api.core.upstream import live_client, upstream_errors 

15 

16# Electricity system data 

17ELECTRICITY_USAGE_DATA = [ 

18 {"id": 1, "name": "北區一號", "capacity": 5200}, 

19 {"id": 2, "name": "北區二號", "capacity": 5600}, 

20 {"id": 3, "name": "仙宮", "capacity": 1500}, 

21] 

22URL_PREFIX = "http://140.114.188.86/powermanage/fn1/kw" 

23URL_POSTFIX = ".aspx" 

24 

25 

26async def fetch_electricity_data(item: dict, client: httpx.AsyncClient) -> dict: 

27 """Async fetch electricity data for one system.""" 

28 url = f"{URL_PREFIX}{item['id']}{URL_POSTFIX}" 

29 with upstream_errors(): 

30 response = await client.get(url) 

31 response.raise_for_status() 

32 

33 data_match = re.search(r"alt=\"kW: ([\d,-]+?)\"", response.text, re.S) 

34 if not data_match: 

35 raise UpstreamResponseException("Electricity usage field is missing") 

36 

37 try: 

38 usage_value = int(data_match.group(1).replace(",", "")) 

39 except ValueError as exc: 

40 raise UpstreamResponseException("Electricity usage is not a number") from exc 

41 return { 

42 **item, 

43 "data": usage_value, 

44 "unit": "kW", 

45 "last_updated": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 

46 } 

47 

48 

49class EnergyService: 

50 """Service for energy data operations.""" 

51 

52 def __init__(self): 

53 self.client: httpx.AsyncClient | None = None 

54 

55 async def get_realtime_electricity_usage(self) -> list[dict]: 

56 """Get real-time electricity usage for all systems.""" 

57 async with live_client(self.client) as client: 

58 tasks = [ 

59 asyncio.create_task(fetch_electricity_data(item, client)) 

60 for item in ELECTRICITY_USAGE_DATA 

61 ] 

62 try: 

63 return await asyncio.gather(*tasks) 

64 finally: 

65 for task in tasks: 

66 if not task.done(): 

67 task.cancel() 

68 await asyncio.gather(*tasks, return_exceptions=True) 

69 

70 

71# Global service instance 

72energy_service = EnergyService()