From ac109579172b41e4826b82b0af2b5772db5a92a7 Mon Sep 17 00:00:00 2001 From: Stephan Uhle Date: Mon, 3 Jun 2024 20:40:47 +0000 Subject: [PATCH] Added some sensors. --- custom_components/talent_monitor/__init__.py | 4 +- .../talent_monitor/config_flow.py | 3 +- .../talent_monitor/coordinator.py | 2 +- custom_components/talent_monitor/entity.py | 4 +- .../pyTalentMonitor/__init__.py | 17 +++---- .../pyTalentMonitor/data_provider.py | 15 +++--- .../pyTalentMonitor/inverter.py | 18 ++++--- .../pyTalentMonitor/power_station.py | 20 ++++---- custom_components/talent_monitor/sensor.py | 51 ++++++++++++++++++- .../talent_monitor/translations/de.json | 19 +++++++ .../talent_monitor/translations/en.json | 19 +++++++ requirements.txt | 4 +- 12 files changed, 131 insertions(+), 45 deletions(-) diff --git a/custom_components/talent_monitor/__init__.py b/custom_components/talent_monitor/__init__.py index 35244fb..2fb5037 100644 --- a/custom_components/talent_monitor/__init__.py +++ b/custom_components/talent_monitor/__init__.py @@ -39,13 +39,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator.async_config_entry_first_refresh() - _LOGGER.debug("received data %s", json.dumps(coordinator.data)) - hass.data[DOMAIN][entry.entry_id] = coordinator for platform in PLATFORMS: coordinator.platforms.append(platform) - hass.async_add_job( + hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) diff --git a/custom_components/talent_monitor/config_flow.py b/custom_components/talent_monitor/config_flow.py index da46ecd..fbbc049 100644 --- a/custom_components/talent_monitor/config_flow.py +++ b/custom_components/talent_monitor/config_flow.py @@ -2,7 +2,8 @@ import logging -from custom_components.talent_monitor.pyTalentMonitor import AuthenticationError, TalentSolarMonitor +from custom_components.talent_monitor.pyTalentMonitor import TalentSolarMonitor +from custom_components.talent_monitor.pyTalentMonitor.data_provider import AuthenticationError import homeassistant.helpers.config_validation as cv import voluptuous as vol from aiohttp import ClientConnectorError diff --git a/custom_components/talent_monitor/coordinator.py b/custom_components/talent_monitor/coordinator.py index 358a573..4937521 100644 --- a/custom_components/talent_monitor/coordinator.py +++ b/custom_components/talent_monitor/coordinator.py @@ -52,7 +52,7 @@ async def _async_update_data(self): """Update data via library.""" _LOGGER.debug("_async_update_data ") try: - return await self.api.fetch_data() + await self.api.fetch_data() except Exception as exception: _LOGGER.exception("_async_update_data failed") raise UpdateFailed() from exception diff --git a/custom_components/talent_monitor/entity.py b/custom_components/talent_monitor/entity.py index 296e7b2..e6920fc 100644 --- a/custom_components/talent_monitor/entity.py +++ b/custom_components/talent_monitor/entity.py @@ -17,7 +17,7 @@ class TalentMonitorEntity(CoordinatorEntity): _attr_has_entity_name = True def __init__( - self, coordinator, entity: Entity + self, coordinator, entity: Entity, entity_suffix: str = "" ): """Initialize a TalentMonitor entity.""" super().__init__(coordinator) @@ -25,7 +25,7 @@ def __init__( device_id = f"{entity.entity_id}" device_name = entity.name - self._attr_unique_id = f"{device_id}" + self._attr_unique_id = f"{device_id}{entity_suffix}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device_id)}, diff --git a/custom_components/talent_monitor/pyTalentMonitor/__init__.py b/custom_components/talent_monitor/pyTalentMonitor/__init__.py index a66d4b3..2b3dc0f 100644 --- a/custom_components/talent_monitor/pyTalentMonitor/__init__.py +++ b/custom_components/talent_monitor/pyTalentMonitor/__init__.py @@ -2,7 +2,6 @@ import argparse import asyncio -import json import logging from aiohttp import ClientSession @@ -13,13 +12,6 @@ # Configure logging _LOGGER: logging.Logger = logging.getLogger(__name__) -BASE_URL = "https://www.talent-monitoring.com/prod-api" -TIMEZONE = "+02:00" - -class AuthenticationError(Exception): - """AuthenticationError when connecting to the Talent API.""" - pass - class TalentSolarMonitor: """TalentSolarMonitor API client.""" @@ -38,13 +30,16 @@ def __init__( def get_power_stations(self) -> list[PowerStation]: return self._power_station_data_provider.power_stations - def fetch_data(self): - self._inverter_data_provider.fetch_data() - self._power_station_data_provider.fetch_data() + async def fetch_data(self): + await self._inverter_data_provider.fetch_data() + await self._power_station_data_provider.fetch_data() async def fetch_solar_data(self): await self.fetch_data() + async def login(self): + await self._data_provider.login() + async def main(username: str, password: str): """Connect to the TalentSolarMonitor API and fetch the solar data.""" async with ClientSession() as session: diff --git a/custom_components/talent_monitor/pyTalentMonitor/data_provider.py b/custom_components/talent_monitor/pyTalentMonitor/data_provider.py index 998cbaa..f31d26e 100644 --- a/custom_components/talent_monitor/pyTalentMonitor/data_provider.py +++ b/custom_components/talent_monitor/pyTalentMonitor/data_provider.py @@ -2,7 +2,6 @@ import os from aiohttp import ClientSession -from custom_components.talent_monitor.pyTalentMonitor import AuthenticationError # Configure logging _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -31,7 +30,7 @@ def get_credentials(self): async def login(self): """Log in using the given credentials.""" login_data = {"username": self._username, "password": self._password} - response = await self.session.post(f"{self._url}/login", json=login_data) + response = await self._session.post(f"{self._url}/login", json=login_data) response_data = await response.json() if "token" in response_data: self._token = response_data["token"] @@ -48,13 +47,13 @@ async def refresh_token(self): async def get_data(self, endpoint): """Get data from the given endpoint.""" if not self._token: - self.login() + await self.login() headers = {"Authorization": f"Bearer {self._token}"} - response = await self.session.get(f"{self._url}/{endpoint}", headers=headers) + response = await self._session.get(f"{self._url}/{endpoint}", headers=headers) if response.status == 401: # Unauthorized, token might be expired self.refresh_token() headers["Authorization"] = f"Bearer {self._token}" - response = await self.session.get(f"{self._url}/{endpoint}", headers=headers) + response = await self._session.get(f"{self._url}/{endpoint}", headers=headers) if response.status == 200: return await response.json() @@ -67,4 +66,8 @@ class Entity: def __init__(self, entity_id: str, name: str) -> None: self.entity_id = entity_id - self.name = name \ No newline at end of file + self.name = name + +class AuthenticationError(Exception): + """AuthenticationError when connecting to the Talent API.""" + pass \ No newline at end of file diff --git a/custom_components/talent_monitor/pyTalentMonitor/inverter.py b/custom_components/talent_monitor/pyTalentMonitor/inverter.py index f795d7e..2b3c1a2 100644 --- a/custom_components/talent_monitor/pyTalentMonitor/inverter.py +++ b/custom_components/talent_monitor/pyTalentMonitor/inverter.py @@ -13,16 +13,16 @@ def __init__( self, entity_id: str, name: str ) -> None: super().__init__(entity_id, name) - self._data - - @property - def data(self, data): - self._data = data + self._data = {} @property def data(self): return self._data + @data.setter + def data(self, data): + self._data = data + class InverterDataProvider(): def __init__( @@ -47,6 +47,8 @@ async def fetch_data(self): if "deviceGuid" in inverter_data: deviceGuid = inverter_data["deviceGuid"] + _LOGGER.debug("Data for inverter GUID %s: %s", deviceGuid, json.dumps(inverter_data)) + if not deviceGuid in self._inverters: self._inverters["deviceGuid"] = Inverter() @@ -56,6 +58,6 @@ async def fetch_data(self): endpoint=f"tools/device/selectDeviceInverterInfo?deviceGuid={deviceGuid}" ) - _LOGGER.debug("Data for inverter GUID %s: %s", deviceGuid, json.dumps(inverter_info)) - if inverter_info: - inverter.data = inverter_info \ No newline at end of file + _LOGGER.debug("Details for inverter GUID %s: %s", deviceGuid, json.dumps(inverter_info)) + if inverter_info and "data" in inverter_info: + inverter.data = inverter_info["data"] \ No newline at end of file diff --git a/custom_components/talent_monitor/pyTalentMonitor/power_station.py b/custom_components/talent_monitor/pyTalentMonitor/power_station.py index 366375c..bb6fa1e 100644 --- a/custom_components/talent_monitor/pyTalentMonitor/power_station.py +++ b/custom_components/talent_monitor/pyTalentMonitor/power_station.py @@ -15,16 +15,16 @@ def __init__( self, entity_id: str, name: str ) -> None: super().__init__(entity_id, name) - self._data - - @property - def data(self, data): - self._data = data + self._data = {} @property def data(self): return self._data + @data.setter + def data(self, data): + self._data = data + class PowerStationDataProvider(): def __init__( @@ -50,15 +50,17 @@ async def fetch_data(self): powerStationGuid = power_station_data["powerStationGuid"] powerStationName = power_station_data["stationName"] + _LOGGER.debug("Data for powerstation GUID %s: %s", powerStationGuid, json.dumps(power_station_data)) + if not powerStationGuid in self._power_stations: self._power_stations["powerStationGuid"] = PowerStation(powerStationGuid, powerStationName) - power_station = self._power_stations["deviceGuid"] + power_station = self._power_stations["powerStationGuid"] power_station_info = await self._data_provider.get_data( endpoint=f"system/station/getPowerStationByGuid?powerStationGuid={powerStationGuid}&timezone={TIMEZONE}" ) - _LOGGER.debug("Data for powerstation GUID %s: %s", powerStationGuid, json.dumps(data)) - if power_station_info: - power_station.data = power_station_info + _LOGGER.debug("Details for powerstation GUID %s: %s", powerStationGuid, json.dumps(power_station_info)) + if power_station_info and "data" in power_station_info: + power_station.data = power_station_info["data"] diff --git a/custom_components/talent_monitor/sensor.py b/custom_components/talent_monitor/sensor.py index 583d469..a6b2cce 100644 --- a/custom_components/talent_monitor/sensor.py +++ b/custom_components/talent_monitor/sensor.py @@ -1,4 +1,6 @@ """Sensor platform for TalentMonitor.""" +from datetime import datetime +import logging from custom_components.talent_monitor.entity import TalentMonitorEntity from custom_components.talent_monitor.pyTalentMonitor.power_station import PowerStation from homeassistant.components.sensor import SensorDeviceClass @@ -10,13 +12,40 @@ from .const import DOMAIN + +_LOGGER: logging.Logger = logging.getLogger(__name__) + SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="totalActivePower", translation_key="talentmonitor_powerstation_totalActivePower", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, - ) + ), + SensorEntityDescription( + key="dayEnergy", + translation_key="talentmonitor_powerstation_dayEnergy", + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="monthEnergy", + translation_key="talentmonitor_powerstation_monthEnergy", + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="yearEnergy", + translation_key="talentmonitor_powerstation_yearEnergy", + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="lastDataUpdateTime", + translation_key="talentmonitor_powerstation_lastDataUpdateTime", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DATE, + ), ) SENSORS = {desc.key: desc for desc in SENSOR_TYPES} @@ -35,6 +64,7 @@ async def async_setup_entry(hass, entry, async_add_devices): power_stations: list[PowerStation] = coordinator.api.get_power_stations() for power_station in power_stations: for index, value in enumerate(power_station.data): + _LOGGER.debug("Iterate data for powerstation %s", value) if value and value in SENSORS: async_add_devices( [ @@ -59,6 +89,7 @@ def __init__( super().__init__( coordinator, power_station, + sensorEntityDescription.key ) self._power_station = power_station @@ -67,5 +98,21 @@ def __init__( @property def native_value(self): """Return the state of the sensor.""" - return self._power_station.data[self.entity_description.key] + if (self.entity_description.key == "lastDataUpdateTime"): + return datetime.fromisoformat(self._power_station.data[self.entity_description.key]) + else: + return self._power_station.data[self.entity_description.key] + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement.""" + key_for_value_with_unit = self.entity_description.key + "Named" + + if (key_for_value_with_unit in self._power_station.data and self._power_station.data[key_for_value_with_unit]): + value_split = self._power_station.data[key_for_value_with_unit].split(" ") + if (value_split and len(value_split) == 2): + unit = value_split[1] + return SENSOR_UNIT_MAPPING[unit] + + return None diff --git a/custom_components/talent_monitor/translations/de.json b/custom_components/talent_monitor/translations/de.json index 9b6a413..4c3196d 100644 --- a/custom_components/talent_monitor/translations/de.json +++ b/custom_components/talent_monitor/translations/de.json @@ -19,5 +19,24 @@ "abort": { "single_instance_allowed": "Es ist nur eine einzige Instanz zulässig." } + }, + "entity": { + "sensor": { + "talentmonitor_powerstation_totalActivePower": { + "name": "Aktuelle Leistung" + }, + "talentmonitor_powerstation_dayEnergy": { + "name": "Energie insgesamt (heute)" + }, + "talentmonitor_powerstation_monthEnergy": { + "name": "Energie insgesamt (dieser Monat)" + }, + "talentmonitor_powerstation_yearEnergy": { + "name": "Energie insgesamt (dieses Jahr)" + }, + "talentmonitor_powerstation_lastDataUpdateTime": { + "name": "Letzte Datenaktualisierung" + } + } } } \ No newline at end of file diff --git a/custom_components/talent_monitor/translations/en.json b/custom_components/talent_monitor/translations/en.json index 9639a57..c15fa92 100644 --- a/custom_components/talent_monitor/translations/en.json +++ b/custom_components/talent_monitor/translations/en.json @@ -19,5 +19,24 @@ "abort": { "single_instance_allowed": "Only a single instance is allowed." } + }, + "entity": { + "sensor": { + "talentmonitor_powerstation_totalActivePower": { + "name": "Current Power" + }, + "talentmonitor_powerstation_dayEnergy": { + "name": "Total Energy Today" + }, + "talentmonitor_powerstation_monthEnergy": { + "name": "Total Energy This Month" + }, + "talentmonitor_powerstation_yearEnergy": { + "name": "Total Energy This Year" + }, + "talentmonitor_powerstation_lastDataUpdateTime": { + "name": "Last Data Update" + } + } } } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f95a664..5c3135d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ colorlog==6.8.2 -homeassistant==2024.5.5 +homeassistant==2024.6.0b5 pip>=21.0,<24.1 ruff==0.4.5 pytest-asyncio -pytest-homeassistant-custom-component==0.13.125 +pytest-homeassistant-custom-component==0.13.129