Skip to content

Commit fa386da

Browse files
committed
wip: Added reading of powerstation sensor.
1 parent c3a8f1e commit fa386da

File tree

9 files changed

+321
-121
lines changed

9 files changed

+321
-121
lines changed

custom_components/talent_monitor/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
# Platforms
1515
SENSOR = "sensor"
16-
PLATFORMS = []
16+
PLATFORMS = [SENSOR]
1717

1818

1919
# Configuration and options

custom_components/talent_monitor/coordinator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async def _async_update_data(self):
5252
"""Update data via library."""
5353
_LOGGER.debug("_async_update_data ")
5454
try:
55-
return await self.api.fetch_solar_data()
55+
return await self.api.fetch_data()
5656
except Exception as exception:
5757
_LOGGER.exception("_async_update_data failed")
5858
raise UpdateFailed() from exception
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""TalentMonitorEntity class."""
2+
import logging
3+
4+
from custom_components.talent_monitor.pyTalentMonitor.data_provider import Entity
5+
from homeassistant.helpers.entity import DeviceInfo
6+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
7+
8+
from .const import DOMAIN
9+
from .const import NAME
10+
11+
_LOGGER: logging.Logger = logging.getLogger(__name__)
12+
13+
14+
class TalentMonitorEntity(CoordinatorEntity):
15+
"""Base Class for TalentMonitor entities."""
16+
17+
_attr_has_entity_name = True
18+
19+
def __init__(
20+
self, coordinator, entity: Entity
21+
):
22+
"""Initialize a TalentMonitor entity."""
23+
super().__init__(coordinator)
24+
25+
device_id = f"{entity.entity_id}"
26+
device_name = entity.name
27+
28+
self._attr_unique_id = f"{device_id}"
29+
30+
self._attr_device_info = DeviceInfo(
31+
identifiers={(DOMAIN, device_id)},
32+
manufacturer=NAME,
33+
name=device_name,
34+
)
35+
36+
_LOGGER.debug("Added TalentMonitor entity id='%s'", self.unique_id)

custom_components/talent_monitor/pyTalentMonitor/__init__.py

Lines changed: 15 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,22 @@
44
import asyncio
55
import json
66
import logging
7-
import os
87

98
from aiohttp import ClientSession
9+
from custom_components.talent_monitor.pyTalentMonitor.inverter import InverterDataProvider
10+
from custom_components.talent_monitor.pyTalentMonitor.data_provider import DataProvider
11+
from custom_components.talent_monitor.pyTalentMonitor.power_station import PowerStation, PowerStationDataProvider
1012

1113
# Configure logging
1214
_LOGGER: logging.Logger = logging.getLogger(__name__)
1315

1416
BASE_URL = "https://www.talent-monitoring.com/prod-api"
1517
TIMEZONE = "+02:00"
1618

17-
1819
class AuthenticationError(Exception):
1920
"""AuthenticationError when connecting to the Talent API."""
20-
2121
pass
2222

23-
2423
class TalentSolarMonitor:
2524
"""TalentSolarMonitor API client."""
2625

@@ -29,125 +28,27 @@ def __init__(
2928
username: str = None,
3029
password: str = None,
3130
session: ClientSession = None,
32-
return_json: bool = False,
3331
):
3432
"""Construct the TalentSolarMonitor API client."""
35-
self.username = username or os.environ.get("PYTALENT_USERNAME")
36-
self.password = password or os.environ.get("PYTALENT_PASSWORD")
37-
self.session = session
38-
self.return_json = return_json
39-
self.token = None
40-
41-
def get_credentials(self):
42-
"""Check whether the credentials are set."""
43-
if not self.username or not self.password:
44-
raise ValueError(
45-
"Credentials not provided via command line arguments or environment variables."
46-
)
33+
self._data_provider = DataProvider(username, password, session)
34+
self._inverter_data_provider = InverterDataProvider(self._data_provider)
35+
self._power_station_data_provider = PowerStationDataProvider(self._data_provider)
4736

48-
async def login(self):
49-
"""Log in using the given credentials."""
50-
login_data = {"username": self.username, "password": self.password}
51-
response = await self.session.post(f"{BASE_URL}/login", json=login_data)
52-
response_data = await response.json()
53-
if "token" in response_data:
54-
self.token = response_data["token"]
55-
_LOGGER.debug("Login successful - received token: %s", self.token)
56-
else:
57-
_LOGGER.error("Login failed. Got status code %s", response.status)
58-
raise AuthenticationError("Authentication failed")
5937

60-
async def refresh_token(self):
61-
"""Refresh the token."""
62-
_LOGGER.debug("Token expired. Refreshing token...")
63-
self.login()
38+
def get_power_stations(self) -> list[PowerStation]:
39+
return self._power_station_data_provider.power_stations
6440

65-
async def get_data(self, endpoint):
66-
"""Get data from the given endpoint."""
67-
if not self.token:
68-
self.login()
69-
headers = {"Authorization": f"Bearer {self.token}"}
70-
response = await self.session.get(f"{BASE_URL}/{endpoint}", headers=headers)
71-
if response.status == 401: # Unauthorized, token might be expired
72-
self.refresh_token()
73-
headers["Authorization"] = f"Bearer {self.token}"
74-
response = await self.session.get(f"{BASE_URL}/{endpoint}", headers=headers)
75-
76-
if response.status == 200:
77-
return await response.json()
78-
else:
79-
_LOGGER.error("Failed to fetch data. Status Code: %s", response.status)
80-
return None
41+
def fetch_data(self):
42+
self._inverter_data_provider.fetch_data()
43+
self._power_station_data_provider.fetch_data()
8144

8245
async def fetch_solar_data(self):
83-
"""Fetch the solar data."""
84-
self.get_credentials()
85-
await self.login()
86-
87-
data = await self.get_data(endpoint="system/station/list")
88-
if data and "rows" in data and len(data["rows"]) > 0:
89-
first_station = data["rows"][0]
90-
status = first_station["status"]
91-
stationName = first_station["stationName"]
92-
powerStationGuid = first_station["powerStationGuid"]
93-
_LOGGER.debug("GUID: %s", powerStationGuid)
94-
95-
data = await self.get_data(
96-
endpoint=f"system/station/getPowerStationByGuid?powerStationGuid={powerStationGuid}&timezone={TIMEZONE}"
97-
)
98-
_LOGGER.debug("Data for powerstation GUID %s: %s", powerStationGuid, json.dumps(data))
99-
100-
if data:
101-
power_data = data["data"]
102-
totalActivePower = power_data["totalActivePower"]
103-
dayEnergy = power_data["dayEnergy"]
104-
monthEnergy = power_data["monthEnergy"]
105-
yearEnergy = power_data["yearEnergy"]
46+
await self.fetch_data()
10647

107-
data = await self.get_data(endpoint="tools/device/selectDeviceInverter")
108-
if data:
109-
deviceGuid = data["rows"][0]["deviceGuid"]
110-
111-
data = await self.get_data(
112-
endpoint=f"tools/device/selectDeviceInverterInfo?deviceGuid={deviceGuid}"
113-
)
114-
115-
_LOGGER.debug("Data for inverter GUID %s: %s", deviceGuid, json.dumps(data))
116-
if data:
117-
pv = data["data"]["pv"]
118-
pv1Voltage = pv[0]["voltage"]
119-
pv1Current = pv[0]["current"]
120-
pv1Power = pv[0]["power"]
121-
pv2Voltage = pv[1]["voltage"]
122-
pv2Current = pv[1]["current"]
123-
pv2Power = pv[1]["power"]
124-
125-
result = {
126-
"Status": status,
127-
"StationName": stationName,
128-
"TotalActivePower(W)": totalActivePower,
129-
"DailyEnergy(Wh)": dayEnergy,
130-
"MonthlyEnergy(Wh)": monthEnergy,
131-
"YearlyEnergy(Wh)": yearEnergy,
132-
"Panel1Voltage(V)": pv1Voltage,
133-
"Panel1Current(A)": pv1Current,
134-
"Panel1Power(W)": pv1Power,
135-
"Panel2Voltage(V)": pv2Voltage,
136-
"Panel2Current(A)": pv2Current,
137-
"Panel2Power(W)": pv2Power,
138-
}
139-
140-
if self.return_json:
141-
return json.dumps(result, indent=4)
142-
else:
143-
for key, value in result.items():
144-
_LOGGER.debug("%s: %s",key, value)
145-
146-
147-
async def main(username: str, password: str, return_json: bool):
48+
async def main(username: str, password: str):
14849
"""Connect to the TalentSolarMonitor API and fetch the solar data."""
14950
async with ClientSession() as session:
150-
talent_monitor = TalentSolarMonitor(username, password, session, return_json)
51+
talent_monitor = TalentSolarMonitor(username, password, session)
15152
result = await talent_monitor.fetch_solar_data()
15253
if result:
15354
_LOGGER.info("Solar data received: %s", result)
@@ -159,9 +60,6 @@ async def main(username: str, password: str, return_json: bool):
15960
)
16061
parser.add_argument("-u", "--username", required=False, help="Username to log in")
16162
parser.add_argument("-p", "--password", required=False, help="Password to log in")
162-
parser.add_argument(
163-
"--json", action="store_true", help="Return output as JSON object"
164-
)
16563
args = parser.parse_args()
16664

167-
asyncio.run(main(args.username, args.password, args.json))
65+
asyncio.run(main(args.username, args.password))
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import logging
2+
import os
3+
4+
from aiohttp import ClientSession
5+
from custom_components.talent_monitor.pyTalentMonitor import AuthenticationError
6+
7+
# Configure logging
8+
_LOGGER: logging.Logger = logging.getLogger(__name__)
9+
10+
BASE_URL = "https://www.talent-monitoring.com/prod-api"
11+
12+
class DataProvider():
13+
"""Data provider accessing the MyGekko API"""
14+
15+
def __init__(
16+
self, username: str, password: str, session: ClientSession
17+
):
18+
self._url = BASE_URL
19+
self._username = username or os.environ.get("PYTALENT_USERNAME")
20+
self._password = password or os.environ.get("PYTALENT_PASSWORD")
21+
self._session = session
22+
self._token = None
23+
24+
def get_credentials(self):
25+
"""Check whether the credentials are set."""
26+
if not self._username or not self.password:
27+
raise ValueError(
28+
"Credentials not provided via command line arguments or environment variables."
29+
)
30+
31+
async def login(self):
32+
"""Log in using the given credentials."""
33+
login_data = {"username": self._username, "password": self._password}
34+
response = await self.session.post(f"{self._url}/login", json=login_data)
35+
response_data = await response.json()
36+
if "token" in response_data:
37+
self._token = response_data["token"]
38+
_LOGGER.debug("Login successful - received token: %s", self._token)
39+
else:
40+
_LOGGER.error("Login failed. Got status code %s", response.status)
41+
raise AuthenticationError("Authentication failed")
42+
43+
async def refresh_token(self):
44+
"""Refresh the token."""
45+
_LOGGER.debug("Token expired. Refreshing token...")
46+
self.login()
47+
48+
async def get_data(self, endpoint):
49+
"""Get data from the given endpoint."""
50+
if not self._token:
51+
self.login()
52+
headers = {"Authorization": f"Bearer {self._token}"}
53+
response = await self.session.get(f"{self._url}/{endpoint}", headers=headers)
54+
if response.status == 401: # Unauthorized, token might be expired
55+
self.refresh_token()
56+
headers["Authorization"] = f"Bearer {self._token}"
57+
response = await self.session.get(f"{self._url}/{endpoint}", headers=headers)
58+
59+
if response.status == 200:
60+
return await response.json()
61+
else:
62+
_LOGGER.error("Failed to fetch data. Status Code: %s", response.status)
63+
return None
64+
65+
class Entity:
66+
"""Base class for TalentMonitor entities"""
67+
68+
def __init__(self, entity_id: str, name: str) -> None:
69+
self.entity_id = entity_id
70+
self.name = name
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""TalentMonitor Inverter"""
2+
3+
import json
4+
import logging
5+
6+
from custom_components.talent_monitor.pyTalentMonitor.data_provider import DataProvider, Entity
7+
8+
# Configure logging
9+
_LOGGER: logging.Logger = logging.getLogger(__name__)
10+
11+
class Inverter(Entity):
12+
def __init__(
13+
self, entity_id: str, name: str
14+
) -> None:
15+
super().__init__(entity_id, name)
16+
self._data
17+
18+
@property
19+
def data(self, data):
20+
self._data = data
21+
22+
@property
23+
def data(self):
24+
return self._data
25+
26+
27+
class InverterDataProvider():
28+
def __init__(
29+
self, data_provider: DataProvider
30+
) -> None:
31+
self._data_provider = data_provider
32+
self._inverter = {}
33+
34+
@property
35+
def inverters(self) -> list[Inverter]:
36+
"""Returns the inverters read from TalentMonitor"""
37+
result: list[Inverter] = []
38+
for key, data in self._inverter.items():
39+
result.append(data)
40+
41+
return result
42+
43+
async def fetch_data(self):
44+
data = await self._data_provider.get_data(endpoint="tools/device/selectDeviceInverter")
45+
if data and "rows" in data:
46+
for inverter_data in data["rows"]:
47+
if "deviceGuid" in inverter_data:
48+
deviceGuid = inverter_data["deviceGuid"]
49+
50+
if not deviceGuid in self._inverters:
51+
self._inverters["deviceGuid"] = Inverter()
52+
53+
inverter = self._inverters["deviceGuid"]
54+
55+
inverter_info = await self._data_provider.get_data(
56+
endpoint=f"tools/device/selectDeviceInverterInfo?deviceGuid={deviceGuid}"
57+
)
58+
59+
_LOGGER.debug("Data for inverter GUID %s: %s", deviceGuid, json.dumps(inverter_info))
60+
if inverter_info:
61+
inverter.data = inverter_info

0 commit comments

Comments
 (0)