From 16b1a6c2cc88d8a2f77435c0acaa45663ea5dc99 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Wed, 18 Oct 2023 20:59:20 +1100 Subject: [PATCH 01/17] A bunch of updates. --- custom_components/sems/sensor.py | 195 +++++++++++++++++-------------- 1 file changed, 105 insertions(+), 90 deletions(-) diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 7729734..6aed96c 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -5,18 +5,11 @@ https://github.com/TimSoethout/goodwe-sems-home-assistant """ -from homeassistant.core import HomeAssistant -import homeassistant import logging - from datetime import timedelta +from decimal import Decimal -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) -from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING, SensorEntity +from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING, SensorEntity, STATE_CLASS_MEASUREMENT from homeassistant.const import ( DEVICE_CLASS_POWER, POWER_WATT, @@ -24,7 +17,12 @@ DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + from .const import DOMAIN, CONF_STATION_ID, DEFAULT_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -119,14 +117,22 @@ async def async_update_data(): # _LOGGER.debug("Initial coordinator data: %s", coordinator.data) async_add_entities( - SemsSensor(coordinator, ent) for idx, ent in enumerate(coordinator.data) + SemsSensor(coordinator, ent) for idx, ent in enumerate(coordinator.data) if ent != "homeKit" ) async_add_entities( SemsStatisticsSensor(coordinator, ent) - for idx, ent in enumerate(coordinator.data) + for idx, ent in enumerate(coordinator.data) if ent != "homeKit" + ) + async_add_entities( + SemsPowerflowSensor(coordinator, ent, "HomeKit Load", "load") + for idx, ent in enumerate(coordinator.data) if ent == "homeKit" + ) + async_add_entities( + SemsPowerflowSensor(coordinator, ent, "HomeKit PV", "pv") + for idx, ent in enumerate(coordinator.data) if ent == "homeKit" ) async_add_entities( - SemsPowerflowSensor(coordinator, ent) + SemsPowerflowSensor(coordinator, ent, "HomeKit Grid", "grid") for idx, ent in enumerate(coordinator.data) if ent == "homeKit" ) async_add_entities( @@ -138,6 +144,7 @@ async def async_update_data(): for idx, ent in enumerate(coordinator.data) if ent == "homeKit" ) + class SemsSensor(CoordinatorEntity, SensorEntity): """SemsSensor using CoordinatorEntity. @@ -160,20 +167,11 @@ def device_class(self): return DEVICE_CLASS_POWER @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): return POWER_WATT @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"Inverter {self.coordinator.data[self.sn]['name']}" - - @property - def unique_id(self) -> str: - return self.coordinator.data[self.sn]["sn"] - - @property - def state(self): + def native_value(self): """Return the state of the device.""" # _LOGGER.debug("state, coordinator data: %s", self.coordinator.data) # _LOGGER.debug("self.sn: %s", self.sn) @@ -181,7 +179,21 @@ def state(self): # "state, self data: %s", self.coordinator.data[self.sn] # ) data = self.coordinator.data[self.sn] - return data["pac"] if data["status"] == 1 else 0 + pac = Decimal(data["pac"]) + return pac if data["status"] == 1 else 0 + + @property + def suggested_display_precision(self): + return 2 + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"Inverter {self.coordinator.data[self.sn]['name']}" + + @property + def unique_id(self) -> str: + return self.coordinator.data[self.sn]["sn"] def statusText(self, status) -> str: labels = {-1: "Offline", 0: "Waiting", 1: "Normal", 2: "Fault"} @@ -200,7 +212,7 @@ def extra_state_attributes(self): @property def is_on(self) -> bool: """Return entity status.""" - self.coordinator.data[self.sn]["status"] == 1 + return self.coordinator.data[self.sn]["status"] == 1 @property def should_poll(self) -> bool: @@ -256,9 +268,24 @@ def device_class(self): return DEVICE_CLASS_ENERGY @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): return ENERGY_KILO_WATT_HOUR + @property + def native_value(self): + """Return the state of the device.""" + # _LOGGER.debug("state, coordinator data: %s", self.coordinator.data) + # _LOGGER.debug("self.sn: %s", self.sn) + # _LOGGER.debug( + # "state, self data: %s", self.coordinator.data[self.sn] + # ) + data = self.coordinator.data[self.sn] + return Decimal(data["etotal"]) + + @property + def suggested_display_precision(self): + return 2 + @property def name(self) -> str: """Return the name of the sensor.""" @@ -268,16 +295,7 @@ def name(self) -> str: def unique_id(self) -> str: return f"{self.coordinator.data[self.sn]['sn']}-energy" - @property - def state(self): - """Return the state of the device.""" - # _LOGGER.debug("state, coordinator data: %s", self.coordinator.data) - # _LOGGER.debug("self.sn: %s", self.sn) - # _LOGGER.debug( - # "state, self data: %s", self.coordinator.data[self.sn] - # ) - data = self.coordinator.data[self.sn] - return data["etotal"] + @property def should_poll(self) -> bool: @@ -318,6 +336,7 @@ async def async_update(self): """ await self.coordinator.async_request_refresh() + class SemsTotalImportSensor(CoordinatorEntity, SensorEntity): """Sensor in kWh to enable HA statistics, in the end usable in the power component.""" @@ -333,9 +352,19 @@ def device_class(self): return DEVICE_CLASS_ENERGY @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): return ENERGY_KILO_WATT_HOUR + @property + def native_value(self): + """Return the state of the device.""" + data = self.coordinator.data[self.sn] + return Decimal(data["Charts_buy"]) + + @property + def suggested_display_precision(self): + return 2 + @property def name(self) -> str: """Return the name of the sensor.""" @@ -345,11 +374,7 @@ def name(self) -> str: def unique_id(self) -> str: return f"{self.coordinator.data[self.sn]['sn']}-import-energy" - @property - def state(self): - """Return the state of the device.""" - data = self.coordinator.data[self.sn] - return data["Charts_buy"] + def statusText(self, status) -> str: labels = {-1: "Offline", 0: "Waiting", 1: "Normal", 2: "Fault"} return labels[status] if status in labels else "Unknown" @@ -389,6 +414,7 @@ async def async_update(self): """ await self.coordinator.async_request_refresh() + class SemsTotalExportSensor(CoordinatorEntity, SensorEntity): """Sensor in kWh to enable HA statistics, in the end usable in the power component.""" @@ -404,9 +430,18 @@ def device_class(self): return DEVICE_CLASS_ENERGY @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): return ENERGY_KILO_WATT_HOUR + @property + def native_value(self): + data = self.coordinator.data[self.sn] + return Decimal(data["Charts_sell"]) + + @property + def suggested_display_precision(self): + return 2 + @property def name(self) -> str: """Return the name of the sensor.""" @@ -416,11 +451,6 @@ def name(self) -> str: def unique_id(self) -> str: return f"{self.coordinator.data[self.sn]['sn']}-export-energy" - @property - def state(self): - """Return the state of the device.""" - data = self.coordinator.data[self.sn] - return data["Charts_sell"] def statusText(self, status) -> str: labels = {-1: "Offline", 0: "Waiting", 1: "Normal", 2: "Fault"} return labels[status] if status in labels else "Unknown" @@ -469,70 +499,55 @@ class SemsPowerflowSensor(CoordinatorEntity, SensorEntity): available """ - def __init__(self, coordinator, sn): + def __init__(self, coordinator, sn, name, key): """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) + self._name = name + self._key = key self.coordinator = coordinator - self.sn = sn + self._sn = sn @property def device_class(self): return DEVICE_CLASS_POWER @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): return POWER_WATT @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"HomeKit {self.coordinator.data[self.sn]['sn']}" + def native_value(self): + powerflow = self.coordinator.data[self._sn] + wats_string = powerflow[self._key] + wats = 0 + if wats_string: + wats_string = wats_string.replace('(W)', '') + wats = int(wats_string) + return wats @property - def unique_id(self) -> str: - return self.coordinator.data[self.sn]["sn"] + def state_class(self): + """used by Metered entities / Long Term Statistics""" + return STATE_CLASS_MEASUREMENT @property - def state(self): - """Return the state of the device.""" - data = self.coordinator.data[self.sn] - load = data["load"] - - if load: - load = load.replace('(W)', '') + def name(self) -> str: + """Return the name of the sensor.""" + return f"{self._name} {self.coordinator.data[self._sn]['sn']}" - return load if data["gridStatus"] == 1 else 0 + @property + def unique_id(self) -> str: + if self._key == '': + return self.coordinator.data[self._sn]["sn"] + return f"{self.coordinator.data[self._sn]['sn']}-{self._key}" def statusText(self, status) -> str: labels = {-1: "Offline", 0: "Waiting", 1: "Normal", 2: "Fault"} return labels[status] if status in labels else "Unknown" - - # For backwards compatibility - @property - def extra_state_attributes(self): - """Return the state attributes of the monitored installation.""" - data = self.coordinator.data[self.sn] - - attributes = {k: v for k, v in data.items() if k is not None and v is not None} - - attributes["pv"] = data["pv"].replace('(W)', '') - attributes["bettery"] = data["bettery"].replace('(W)', '') - attributes["load"] = data["load"].replace('(W)', '') - attributes["grid"] = data["grid"].replace('(W)', '') - - attributes["statusText"] = self.statusText(data["gridStatus"]) - - if data['loadStatus'] == -1 : - attributes['PowerFlowDirection'] = 'Export %s' % data['grid'] - if data['loadStatus'] == 1 : - attributes['PowerFlowDirection'] = 'Import %s' % data['grid'] - - return attributes - @property def is_on(self) -> bool: """Return entity status.""" - self.coordinator.data[self.sn]["gridStatus"] == 1 + return self.coordinator.data[self._sn][self._key + "Status"] == 1 @property def should_poll(self) -> bool: @@ -549,7 +564,7 @@ def device_info(self): return { "identifiers": { # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.sn) + (DOMAIN, self._sn) }, "name": "Homekit", "manufacturer": "GoodWe", From 2d726953d0bc327edd2e373818d7d63d96bed828 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Sat, 21 Oct 2023 18:15:19 +1100 Subject: [PATCH 02/17] move everything to sensors. --- custom_components/sems/const.py | 18 +- custom_components/sems/sensor.py | 935 ++++++++++++++----------------- 2 files changed, 446 insertions(+), 507 deletions(-) diff --git a/custom_components/sems/const.py b/custom_components/sems/const.py index 684952a..9da008d 100644 --- a/custom_components/sems/const.py +++ b/custom_components/sems/const.py @@ -3,9 +3,7 @@ DOMAIN = "sems" import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL -from datetime import timedelta CONF_STATION_ID = "powerstation_id" @@ -22,3 +20,19 @@ ): int, # , default=DEFAULT_SCAN_INTERVAL } ) + +API_UPDATE_ERROR_MSG = "Error communicating with API, probably token could not be fetched, see debug logs" + +AC_EMPTY = 6553.5 +AC_CURRENT_EMPTY = 6553.5 +AC_FEQ_EMPTY = 655.35 + +class GOODWE_SPELLING: + battery = "bettery" + homeKit = "homKit" + temperature = "tempperature" + hasEnergyStatisticsCharts = "hasEnergeStatisticsCharts" + energyStatisticsCharts = "energeStatisticsCharts" + energyStatisticsTotals = "energeStatisticsTotals" + thisMonthTotalE = "thismonthetotle" + lastMonthTotalE = "lastmonthetotle" diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 6aed96c..8c8e389 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -6,35 +6,87 @@ """ import logging +import re from datetime import timedelta from decimal import Decimal +from typing import List, Callable -from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING, SensorEntity, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import SensorEntity, SensorDeviceClass, SensorStateClass +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - DEVICE_CLASS_POWER, POWER_WATT, CONF_SCAN_INTERVAL, - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, + ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, ELECTRIC_POTENTIAL_VOLT, ELECTRIC_CURRENT_AMPERE, FREQUENCY_HERTZ, PERCENTAGE, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, UpdateFailed, ) -from .const import DOMAIN, CONF_STATION_ID, DEFAULT_SCAN_INTERVAL +from .const import DOMAIN, CONF_STATION_ID, DEFAULT_SCAN_INTERVAL, API_UPDATE_ERROR_MSG, GOODWE_SPELLING, AC_EMPTY, \ + AC_CURRENT_EMPTY, AC_FEQ_EMPTY _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +def get_value_from_path(data, path): + value = data + for key in path: + value = value[key] + return value + + +class Sensor(CoordinatorEntity, SensorEntity): + str_clean_regex = re.compile(r"(\d+\.?\d*)") + + def __init__(self, coordinator, device_info: DeviceInfo, serial_number, unique_id: str, name: str, + value_path: List[str], data_type_converter: Callable, device_class: SensorDeviceClass, + native_unit_of_measurement: str, state_class: SensorStateClass, empty_value=None): + super().__init__(coordinator) + self._sn = serial_number + self._value_path = value_path + self._data_type_converter = data_type_converter + self._empty_value = empty_value + + self._attr_unique_id = unique_id + self._attr_name = name + self._attr_native_unit_of_measurement = native_unit_of_measurement + self._attr_device_class = device_class + self._attr_state_class = state_class + self._attr_device_info = device_info + + def _get_native_value_from_coordinator(self): + return get_value_from_path(self.coordinator.data, self._value_path) + + @property + def native_value(self): + """Return the state of the device.""" + value = get_value_from_path(self.coordinator.data, self._value_path) + if isinstance(value, str): + value = self.str_clean_regex.search(value).group(1) + if self._empty_value is not None and self._empty_value == value: + value = 0 + typed_value = self._data_type_converter(value) + return typed_value + + @property + def suggested_display_precision(self): + return 2 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Add sensors for passed config_entry in HA.""" - # _LOGGER.debug("hass.data[DOMAIN] %s", hass.data[DOMAIN]) semsApi = hass.data[DOMAIN][config_entry.entry_id] stationId = config_entry.data[CONF_STATION_ID] - # _LOGGER.debug("config_entry %s", config_entry.data) update_interval = timedelta( seconds=config_entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) ) @@ -52,43 +104,12 @@ async def async_update_data(): result = await hass.async_add_executor_job(semsApi.getData, stationId) _LOGGER.debug("Resulting result: %s", result) + if "inverter" not in result: + raise UpdateFailed(API_UPDATE_ERROR_MSG) inverters = result["inverter"] - - # found = [] - # _LOGGER.debug("Found inverters: %s", inverters) - data = {} if inverters is None: - # something went wrong, probably token could not be fetched - raise UpdateFailed( - "Error communicating with API, probably token could not be fetched, see debug logs" - ) - for inverter in inverters: - name = inverter["invert_full"]["name"] - # powerstation_id = inverter["invert_full"]["powerstation_id"] - sn = inverter["invert_full"]["sn"] - _LOGGER.debug("Found inverter attribute %s %s", name, sn) - data[sn] = inverter["invert_full"] - - hasPowerflow = result["hasPowerflow"] - hasEnergeStatisticsCharts = result["hasEnergeStatisticsCharts"] - - if hasPowerflow: - if hasEnergeStatisticsCharts: - StatisticsCharts = { f"Charts_{key}": val for key, val in result["energeStatisticsCharts"].items() } - StatisticsTotals = { f"Totals_{key}": val for key, val in result["energeStatisticsTotals"].items() } - powerflow = { **result["powerflow"], **StatisticsCharts, **StatisticsTotals } - else: - powerflow = result["powerflow"] - - powerflow["sn"] = result["homKit"]["sn"] - #_LOGGER.debug("homeKit sn: %s", result["homKit"]["sn"]) - # This seems more accurate than the Chart_sum - powerflow["all_time_generation"] = result["kpi"]["total_power"] - - data["homeKit"] = powerflow - - #_LOGGER.debug("Resulting data: %s", data) - return data + raise UpdateFailed(API_UPDATE_ERROR_MSG) + return result # except ApiError as err: except Exception as err: # logging.exception("Something awful happened!") @@ -115,470 +136,374 @@ async def async_update_data(): # await coordinator.async_config_entry_first_refresh() - # _LOGGER.debug("Initial coordinator data: %s", coordinator.data) - async_add_entities( - SemsSensor(coordinator, ent) for idx, ent in enumerate(coordinator.data) if ent != "homeKit" - ) - async_add_entities( - SemsStatisticsSensor(coordinator, ent) - for idx, ent in enumerate(coordinator.data) if ent != "homeKit" - ) - async_add_entities( - SemsPowerflowSensor(coordinator, ent, "HomeKit Load", "load") - for idx, ent in enumerate(coordinator.data) if ent == "homeKit" - ) - async_add_entities( - SemsPowerflowSensor(coordinator, ent, "HomeKit PV", "pv") - for idx, ent in enumerate(coordinator.data) if ent == "homeKit" + data = coordinator.data + + try: + currency = data['kpi']['currency'] + except KeyError: + currency = None + + cloud_id = "sems" + device_info = DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, cloud_id) + }, + name="SEMS Cloud", + manufacturer="GoodWe", ) - async_add_entities( - SemsPowerflowSensor(coordinator, ent, "HomeKit Grid", "grid") - for idx, ent in enumerate(coordinator.data) if ent == "homeKit" - ) - async_add_entities( - SemsTotalImportSensor(coordinator, ent) - for idx, ent in enumerate(coordinator.data) if ent == "homeKit" - ) - async_add_entities( - SemsTotalExportSensor(coordinator, ent) - for idx, ent in enumerate(coordinator.data) if ent == "homeKit" - ) - - -class SemsSensor(CoordinatorEntity, SensorEntity): - """SemsSensor using CoordinatorEntity. - - The CoordinatorEntity class provides: - should_poll - async_update - async_added_to_hass - available - """ - - def __init__(self, coordinator, sn): - """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator) - self.coordinator = coordinator - self.sn = sn - _LOGGER.debug("Creating SemsSensor with id %s", self.sn) - - @property - def device_class(self): - return DEVICE_CLASS_POWER - - @property - def native_unit_of_measurement(self): - return POWER_WATT - - @property - def native_value(self): - """Return the state of the device.""" - # _LOGGER.debug("state, coordinator data: %s", self.coordinator.data) - # _LOGGER.debug("self.sn: %s", self.sn) - # _LOGGER.debug( - # "state, self data: %s", self.coordinator.data[self.sn] - # ) - data = self.coordinator.data[self.sn] - pac = Decimal(data["pac"]) - return pac if data["status"] == 1 else 0 - - @property - def suggested_display_precision(self): - return 2 - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"Inverter {self.coordinator.data[self.sn]['name']}" - - @property - def unique_id(self) -> str: - return self.coordinator.data[self.sn]["sn"] - - def statusText(self, status) -> str: - labels = {-1: "Offline", 0: "Waiting", 1: "Normal", 2: "Fault"} - return labels[status] if status in labels else "Unknown" - # For backwards compatibility - @property - def extra_state_attributes(self): - """Return the state attributes of the monitored installation.""" - data = self.coordinator.data[self.sn] - # _LOGGER.debug("state, self data: %s", data.items()) - attributes = {k: v for k, v in data.items() if k is not None and v is not None} - attributes["statusText"] = self.statusText(data["status"]) - return attributes - - @property - def is_on(self) -> bool: - """Return entity status.""" - return self.coordinator.data[self.sn]["status"] == 1 - - @property - def should_poll(self) -> bool: - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self.coordinator.last_update_success - - @property - def device_info(self): - # _LOGGER.debug("self.device_state_attributes: %s", self.device_state_attributes) - return { - "identifiers": { - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.sn) - }, - "name": self.name, - "manufacturer": "GoodWe", - "model": self.extra_state_attributes.get("model_type", "unknown"), - "sw_version": self.extra_state_attributes.get("firmwareversion", "unknown"), - # "via_device": (DOMAIN, self.api.bridgeid), - } - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() - - -class SemsStatisticsSensor(CoordinatorEntity, SensorEntity): - """Sensor in kWh to enable HA statistics, in the end usable in the power component.""" - - def __init__(self, coordinator, sn): - """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator) - self.coordinator = coordinator - self.sn = sn - _LOGGER.debug("Creating SemsStatisticsSensor with id %s", self.sn) - - @property - def device_class(self): - return DEVICE_CLASS_ENERGY - - @property - def native_unit_of_measurement(self): - return ENERGY_KILO_WATT_HOUR - - @property - def native_value(self): - """Return the state of the device.""" - # _LOGGER.debug("state, coordinator data: %s", self.coordinator.data) - # _LOGGER.debug("self.sn: %s", self.sn) - # _LOGGER.debug( - # "state, self data: %s", self.coordinator.data[self.sn] - # ) - data = self.coordinator.data[self.sn] - return Decimal(data["etotal"]) - - @property - def suggested_display_precision(self): - return 2 - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"Inverter {self.coordinator.data[self.sn]['name']} Energy" - - @property - def unique_id(self) -> str: - return f"{self.coordinator.data[self.sn]['sn']}-energy" - - - - @property - def should_poll(self) -> bool: - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def device_info(self): - # _LOGGER.debug("self.device_state_attributes: %s", self.device_state_attributes) - data = self.coordinator.data[self.sn] - return { - "identifiers": { + for idx, inverter in enumerate(data["inverter"]): + serial_number = inverter['sn'] + path_to_inverter = ["inverter", idx, "invert_full"] + name = inverter.get('name', 'unknown') + device_data = get_value_from_path(coordinator.data, path_to_inverter) + device_info = DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.sn) + (DOMAIN, serial_number) }, - # "name": self.name, - "manufacturer": "GoodWe", - "model": data.get("model_type", "unknown"), - "sw_version": data.get("firmwareversion", "unknown"), - # "via_device": (DOMAIN, self.api.bridgeid), - } - - @property - def state_class(self): - """used by Metered entities / Long Term Statistics""" - return STATE_CLASS_TOTAL_INCREASING - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) + name=name, + manufacturer="GoodWe", + model=device_data.get("model_type", "unknown"), + sw_version=device_data.get("firmwareversion", "unknown"), ) - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() - - -class SemsTotalImportSensor(CoordinatorEntity, SensorEntity): - """Sensor in kWh to enable HA statistics, in the end usable in the power component.""" - - def __init__(self, coordinator, sn): - """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator) - self.coordinator = coordinator - self.sn = sn - _LOGGER.debug("Creating SemsStatisticsSensor with id %s", self.sn) - - @property - def device_class(self): - return DEVICE_CLASS_ENERGY - - @property - def native_unit_of_measurement(self): - return ENERGY_KILO_WATT_HOUR - - @property - def native_value(self): - """Return the state of the device.""" - data = self.coordinator.data[self.sn] - return Decimal(data["Charts_buy"]) - - @property - def suggested_display_precision(self): - return 2 - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"HomeKit {self.coordinator.data[self.sn]['sn']} Import" - - @property - def unique_id(self) -> str: - return f"{self.coordinator.data[self.sn]['sn']}-import-energy" - - - def statusText(self, status) -> str: - labels = {-1: "Offline", 0: "Waiting", 1: "Normal", 2: "Fault"} - return labels[status] if status in labels else "Unknown" - - - @property - def should_poll(self) -> bool: - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def device_info(self): - return { - "identifiers": { - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.sn) - }, - "name": "Homekit", - "manufacturer": "GoodWe", - } - - @property - def state_class(self): - """used by Metered entities / Long Term Statistics""" - return STATE_CLASS_TOTAL_INCREASING - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() - - -class SemsTotalExportSensor(CoordinatorEntity, SensorEntity): - """Sensor in kWh to enable HA statistics, in the end usable in the power component.""" - - def __init__(self, coordinator, sn): - """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator) - self.coordinator = coordinator - self.sn = sn - _LOGGER.debug("Creating SemsStatisticsSensor with id %s", self.sn) - - @property - def device_class(self): - return DEVICE_CLASS_ENERGY - - @property - def native_unit_of_measurement(self): - return ENERGY_KILO_WATT_HOUR - - @property - def native_value(self): - data = self.coordinator.data[self.sn] - return Decimal(data["Charts_sell"]) - - @property - def suggested_display_precision(self): - return 2 - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"HomeKit {self.coordinator.data[self.sn]['sn']} Export" - - @property - def unique_id(self) -> str: - return f"{self.coordinator.data[self.sn]['sn']}-export-energy" - - def statusText(self, status) -> str: - labels = {-1: "Offline", 0: "Waiting", 1: "Normal", 2: "Fault"} - return labels[status] if status in labels else "Unknown" - - @property - def should_poll(self) -> bool: - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def device_info(self): - return { - "identifiers": { - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.sn) - }, - "name": "Homekit", - "manufacturer": "GoodWe", - } - - @property - def state_class(self): - """used by Metered entities / Long Term Statistics""" - return STATE_CLASS_TOTAL_INCREASING - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() - -class SemsPowerflowSensor(CoordinatorEntity, SensorEntity): - """SemsPowerflowSensor using CoordinatorEntity. - - The CoordinatorEntity class provides: - should_poll - async_update - async_added_to_hass - available - """ - - def __init__(self, coordinator, sn, name, key): - """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator) - self._name = name - self._key = key - self.coordinator = coordinator - self._sn = sn - - @property - def device_class(self): - return DEVICE_CLASS_POWER - - @property - def native_unit_of_measurement(self): - return POWER_WATT - - @property - def native_value(self): - powerflow = self.coordinator.data[self._sn] - wats_string = powerflow[self._key] - wats = 0 - if wats_string: - wats_string = wats_string.replace('(W)', '') - wats = int(wats_string) - return wats - - @property - def state_class(self): - """used by Metered entities / Long Term Statistics""" - return STATE_CLASS_MEASUREMENT - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._name} {self.coordinator.data[self._sn]['sn']}" - - @property - def unique_id(self) -> str: - if self._key == '': - return self.coordinator.data[self._sn]["sn"] - return f"{self.coordinator.data[self._sn]['sn']}-{self._key}" - - def statusText(self, status) -> str: - labels = {-1: "Offline", 0: "Waiting", 1: "Normal", 2: "Fault"} - return labels[status] if status in labels else "Unknown" - @property - def is_on(self) -> bool: - """Return entity status.""" - return self.coordinator.data[self._sn][self._key + "Status"] == 1 - - @property - def should_poll(self) -> bool: - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self.coordinator.last_update_success - - @property - def device_info(self): - return { - "identifiers": { - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self._sn) - }, - "name": "Homekit", - "manufacturer": "GoodWe", - } - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() + async_add_entities([ + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-capacity", + f"Inverter {inverter['name']} Capacity", + path_to_inverter + ["capacity"], + Decimal, + SensorDeviceClass.POWER, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.MEASUREMENT + ), + Sensor( + coordinator, + device_info, + serial_number, + serial_number, + # should be {serial_number}-pac but is serial_number for backwards compatibility + f"Inverter {inverter['name']} Power", + path_to_inverter + ["pac"], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-energy", + # should be {serial_number}-etotal but is {serial_number}-energy for backwards compatibility + f"Inverter {inverter['name']} Energy", + path_to_inverter + ["etotal"], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-temperature", + f"Inverter {inverter['name']} Temperature", + path_to_inverter + [GOODWE_SPELLING.temperature], + Decimal, + SensorDeviceClass.TEMPERATURE, + TEMP_CELSIUS, + SensorStateClass.MEASUREMENT + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-eday ", + f"Inverter {inverter['name']} Energy Today", + path_to_inverter + ["eday"], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-{GOODWE_SPELLING.thisMonthTotalE}", + f"Inverter {inverter['name']} Energy This Month", + path_to_inverter + [GOODWE_SPELLING.thisMonthTotalE], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-{GOODWE_SPELLING.lastMonthTotalE}", + f"Inverter {inverter['name']} Energy Last Month", + path_to_inverter + [GOODWE_SPELLING.lastMonthTotalE], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-iday", + f"Inverter {inverter['name']} Income Today", + path_to_inverter + ["iday"], + Decimal, + SensorDeviceClass.MONETARY, + currency, + SensorStateClass.TOTAL_INCREASING, + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-itotal", + f"Inverter {inverter['name']} Income Total", + path_to_inverter + ["itotal"], + Decimal, + SensorDeviceClass.MONETARY, + currency, + SensorStateClass.TOTAL_INCREASING, + ), + ] + [ + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-vpv{idx}", + f"Inverter {inverter['name']} PV String {idx} Voltage", + path_to_inverter + [f"vpv{idx}"], + Decimal, + SensorDeviceClass.VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT + ) for idx in range(1, 5) + ] + [ + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-ipv{idx}", + f"Inverter {inverter['name']} PV String {idx} Current", + path_to_inverter + [f"ipv{idx}"], + Decimal, + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT + ) for idx in range(1, 5) + ] + [ + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-vac{idx}", + f"Inverter {inverter['name']} {idx} AC Voltage", + path_to_inverter + [f"vac{idx}"], + Decimal, + SensorDeviceClass.VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + AC_EMPTY + ) for idx in range(1, 4) + ] + [ + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-iac{idx}", + f"Inverter {inverter['name']} Grid {idx} AC Current", + path_to_inverter + [f"iac{idx}"], + Decimal, + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + AC_CURRENT_EMPTY + ) for idx in range(1, 4) + ] + [ + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-fac{idx}", + f"Inverter {inverter['name']} Grid {idx} AC Frequency", + path_to_inverter + [f"fac{idx}"], + Decimal, + SensorDeviceClass.FREQUENCY, + FREQUENCY_HERTZ, + SensorStateClass.MEASUREMENT, + AC_FEQ_EMPTY + ) for idx in range(1, 4) + ] + [ + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-vbattery1", + f"Inverter {inverter['name']} Battery Voltage", + path_to_inverter + ["vbattery1"], + Decimal, + SensorDeviceClass.VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-ibattery1", + f"Inverter {inverter['name']} Battery Current", + path_to_inverter + ["ibattery1"], + Decimal, + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT + ), + ]) + + if "hasPowerflow" in data and data["hasPowerflow"] and 'powerflow' in data: + serial_number = data[GOODWE_SPELLING.homeKit]["sn"] + async_add_entities([ + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}", # should be {serial_number}-load but is {serial_number} for backwards compatibility + f"HomeKit Load", + ["powerflow", "load"], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-pv", + f"HomeKit PV", + ["powerflow", "pv"], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-grid", + f"HomeKit Grid", + ["powerflow", "grid"], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-battery", + f"HomeKit Battery", + ["powerflow" + GOODWE_SPELLING.battery], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-genset", + f"HomeKit generator", + ["powerflow", "genset"], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-soc", + f"HomeKit State of Charge", + ["powerflow", "soc"], + Decimal, + SensorDeviceClass.BATTERY, + PERCENTAGE, + SensorStateClass.MEASUREMENT, + ), + ]) + if GOODWE_SPELLING.hasEnergyStatisticsCharts in data and data[GOODWE_SPELLING.hasEnergyStatisticsCharts]: + if data[GOODWE_SPELLING.energyStatisticsCharts]: + async_add_entities([ + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-import-energy", + # should be {serial_number}-import but is {serial_number}-import-energy for backwards compatibility + f"Sems Import", + [GOODWE_SPELLING.energyStatisticsCharts, "buy"], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-export-energy", + # should be {serial_number}-export but is {serial_number}-export-energy for backwards compatibility + f"Sems Export", + [GOODWE_SPELLING.energyStatisticsCharts, "sell"], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING + ), + ]) + if data[GOODWE_SPELLING.energyStatisticsTotals]: + async_add_entities([ + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-import-energy-total", + f"Sems Total Import", + [GOODWE_SPELLING.energyStatisticsTotals, "buy"], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING + ), + Sensor( + coordinator, + device_info, + serial_number, + f"{serial_number}-export-energy-total", + f"Sems Total Export", + [GOODWE_SPELLING.energyStatisticsTotals, "sell"], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING + ), + ]) From 9fd794a9054f39b9051970d9c496ff33216fb236 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Sat, 21 Oct 2023 19:26:28 +1100 Subject: [PATCH 03/17] Formatting --- custom_components/sems/sensor.py | 696 ++++++++++++++++--------------- 1 file changed, 361 insertions(+), 335 deletions(-) diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 8c8e389..cd12d81 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -9,14 +9,24 @@ import re from datetime import timedelta from decimal import Decimal -from typing import List, Callable +from typing import List, Callable, Optional -from homeassistant.components.sensor import SensorEntity, SensorDeviceClass, SensorStateClass +from homeassistant.components.sensor import ( + SensorEntity, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( POWER_WATT, CONF_SCAN_INTERVAL, - ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, ELECTRIC_POTENTIAL_VOLT, ELECTRIC_CURRENT_AMPERE, FREQUENCY_HERTZ, PERCENTAGE, + ENERGY_KILO_WATT_HOUR, + TEMP_CELSIUS, + ELECTRIC_POTENTIAL_VOLT, + ELECTRIC_CURRENT_AMPERE, + FREQUENCY_HERTZ, + PERCENTAGE, + POWER_KILO_WATT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -27,8 +37,16 @@ UpdateFailed, ) -from .const import DOMAIN, CONF_STATION_ID, DEFAULT_SCAN_INTERVAL, API_UPDATE_ERROR_MSG, GOODWE_SPELLING, AC_EMPTY, \ - AC_CURRENT_EMPTY, AC_FEQ_EMPTY +from .const import ( + DOMAIN, + CONF_STATION_ID, + DEFAULT_SCAN_INTERVAL, + API_UPDATE_ERROR_MSG, + GOODWE_SPELLING, + AC_EMPTY, + AC_CURRENT_EMPTY, + AC_FEQ_EMPTY, +) _LOGGER = logging.getLogger(__name__) @@ -43,11 +61,20 @@ def get_value_from_path(data, path): class Sensor(CoordinatorEntity, SensorEntity): str_clean_regex = re.compile(r"(\d+\.?\d*)") - def __init__(self, coordinator, device_info: DeviceInfo, serial_number, unique_id: str, name: str, - value_path: List[str], data_type_converter: Callable, device_class: SensorDeviceClass, - native_unit_of_measurement: str, state_class: SensorStateClass, empty_value=None): + def __init__( + self, + coordinator, + device_info: DeviceInfo, + unique_id: str, + name: str, + value_path: List[str], + data_type_converter: Callable, + device_class: Optional[SensorDeviceClass] = None, + native_unit_of_measurement: Optional[str] = None, + state_class: Optional[SensorStateClass] = None, + empty_value=None, + ): super().__init__(coordinator) - self._sn = serial_number self._value_path = value_path self._data_type_converter = data_type_converter self._empty_value = empty_value @@ -79,9 +106,9 @@ def suggested_display_precision(self): async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add sensors for passed config_entry in HA.""" semsApi = hass.data[DOMAIN][config_entry.entry_id] @@ -139,24 +166,14 @@ async def async_update_data(): data = coordinator.data try: - currency = data['kpi']['currency'] + currency = data["kpi"]["currency"] except KeyError: currency = None - cloud_id = "sems" - device_info = DeviceInfo( - identifiers={ - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, cloud_id) - }, - name="SEMS Cloud", - manufacturer="GoodWe", - ) - for idx, inverter in enumerate(data["inverter"]): - serial_number = inverter['sn'] + serial_number = inverter["sn"] path_to_inverter = ["inverter", idx, "invert_full"] - name = inverter.get('name', 'unknown') + name = inverter.get("name", "unknown") device_data = get_value_from_path(coordinator.data, path_to_inverter) device_info = DeviceInfo( identifiers={ @@ -168,342 +185,351 @@ async def async_update_data(): model=device_data.get("model_type", "unknown"), sw_version=device_data.get("firmwareversion", "unknown"), ) - async_add_entities([ - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-capacity", - f"Inverter {inverter['name']} Capacity", - path_to_inverter + ["capacity"], - Decimal, - SensorDeviceClass.POWER, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.MEASUREMENT - ), - Sensor( - coordinator, - device_info, - serial_number, - serial_number, - # should be {serial_number}-pac but is serial_number for backwards compatibility - f"Inverter {inverter['name']} Power", - path_to_inverter + ["pac"], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-energy", - # should be {serial_number}-etotal but is {serial_number}-energy for backwards compatibility - f"Inverter {inverter['name']} Energy", - path_to_inverter + ["etotal"], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-temperature", - f"Inverter {inverter['name']} Temperature", - path_to_inverter + [GOODWE_SPELLING.temperature], - Decimal, - SensorDeviceClass.TEMPERATURE, - TEMP_CELSIUS, - SensorStateClass.MEASUREMENT - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-eday ", - f"Inverter {inverter['name']} Energy Today", - path_to_inverter + ["eday"], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-{GOODWE_SPELLING.thisMonthTotalE}", - f"Inverter {inverter['name']} Energy This Month", - path_to_inverter + [GOODWE_SPELLING.thisMonthTotalE], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-{GOODWE_SPELLING.lastMonthTotalE}", - f"Inverter {inverter['name']} Energy Last Month", - path_to_inverter + [GOODWE_SPELLING.lastMonthTotalE], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-iday", - f"Inverter {inverter['name']} Income Today", - path_to_inverter + ["iday"], - Decimal, - SensorDeviceClass.MONETARY, - currency, - SensorStateClass.TOTAL_INCREASING, - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-itotal", - f"Inverter {inverter['name']} Income Total", - path_to_inverter + ["itotal"], - Decimal, - SensorDeviceClass.MONETARY, - currency, - SensorStateClass.TOTAL_INCREASING, - ), - ] + [ - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-vpv{idx}", - f"Inverter {inverter['name']} PV String {idx} Voltage", - path_to_inverter + [f"vpv{idx}"], - Decimal, - SensorDeviceClass.VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, - SensorStateClass.MEASUREMENT - ) for idx in range(1, 5) - ] + [ - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-ipv{idx}", - f"Inverter {inverter['name']} PV String {idx} Current", - path_to_inverter + [f"ipv{idx}"], - Decimal, - SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, - SensorStateClass.MEASUREMENT - ) for idx in range(1, 5) - ] + [ - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-vac{idx}", - f"Inverter {inverter['name']} {idx} AC Voltage", - path_to_inverter + [f"vac{idx}"], - Decimal, - SensorDeviceClass.VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, - SensorStateClass.MEASUREMENT, - AC_EMPTY - ) for idx in range(1, 4) - ] + [ - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-iac{idx}", - f"Inverter {inverter['name']} Grid {idx} AC Current", - path_to_inverter + [f"iac{idx}"], - Decimal, - SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, - SensorStateClass.MEASUREMENT, - AC_CURRENT_EMPTY - ) for idx in range(1, 4) - ] + [ - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-fac{idx}", - f"Inverter {inverter['name']} Grid {idx} AC Frequency", - path_to_inverter + [f"fac{idx}"], - Decimal, - SensorDeviceClass.FREQUENCY, - FREQUENCY_HERTZ, - SensorStateClass.MEASUREMENT, - AC_FEQ_EMPTY - ) for idx in range(1, 4) - ] + [ - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-vbattery1", - f"Inverter {inverter['name']} Battery Voltage", - path_to_inverter + ["vbattery1"], - Decimal, - SensorDeviceClass.VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, - SensorStateClass.MEASUREMENT - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-ibattery1", - f"Inverter {inverter['name']} Battery Current", - path_to_inverter + ["ibattery1"], - Decimal, - SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, - SensorStateClass.MEASUREMENT - ), - ]) - - if "hasPowerflow" in data and data["hasPowerflow"] and 'powerflow' in data: - serial_number = data[GOODWE_SPELLING.homeKit]["sn"] - async_add_entities([ - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}", # should be {serial_number}-load but is {serial_number} for backwards compatibility - f"HomeKit Load", - ["powerflow", "load"], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-pv", - f"HomeKit PV", - ["powerflow", "pv"], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-grid", - f"HomeKit Grid", - ["powerflow", "grid"], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-battery", - f"HomeKit Battery", - ["powerflow" + GOODWE_SPELLING.battery], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT, - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-genset", - f"HomeKit generator", - ["powerflow", "genset"], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT - ), - Sensor( - coordinator, - device_info, - serial_number, - f"{serial_number}-soc", - f"HomeKit State of Charge", - ["powerflow", "soc"], - Decimal, - SensorDeviceClass.BATTERY, - PERCENTAGE, - SensorStateClass.MEASUREMENT, - ), - ]) - if GOODWE_SPELLING.hasEnergyStatisticsCharts in data and data[GOODWE_SPELLING.hasEnergyStatisticsCharts]: - if data[GOODWE_SPELLING.energyStatisticsCharts]: - async_add_entities([ + async_add_entities( + [ + Sensor( + coordinator, + device_info, + f"{serial_number}-capacity", + f"Inverter {inverter['name']} Capacity", + path_to_inverter + ["capacity"], + Decimal, + SensorDeviceClass.POWER, + POWER_KILO_WATT, + SensorStateClass.MEASUREMENT, + ), Sensor( coordinator, device_info, serial_number, - f"{serial_number}-import-energy", - # should be {serial_number}-import but is {serial_number}-import-energy for backwards compatibility - f"Sems Import", - [GOODWE_SPELLING.energyStatisticsCharts, "buy"], + f"Inverter {inverter['name']} Power", + path_to_inverter + ["pac"], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-energy", + f"Inverter {inverter['name']} Energy", + path_to_inverter + ["etotal"], Decimal, SensorDeviceClass.ENERGY, ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING + SensorStateClass.TOTAL_INCREASING, ), Sensor( coordinator, device_info, - serial_number, - f"{serial_number}-export-energy", - # should be {serial_number}-export but is {serial_number}-export-energy for backwards compatibility - f"Sems Export", - [GOODWE_SPELLING.energyStatisticsCharts, "sell"], + f"{serial_number}-hour-total", + f"Inverter {inverter['name']} Total Hours", + path_to_inverter + ["hour_total"], + Decimal, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-temperature", + f"Inverter {inverter['name']} Temperature", + path_to_inverter + [GOODWE_SPELLING.temperature], + Decimal, + SensorDeviceClass.TEMPERATURE, + TEMP_CELSIUS, + SensorStateClass.MEASUREMENT, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-eday ", + f"Inverter {inverter['name']} Energy Today", + path_to_inverter + ["eday"], Decimal, SensorDeviceClass.ENERGY, ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING + SensorStateClass.TOTAL_INCREASING, ), - ]) - if data[GOODWE_SPELLING.energyStatisticsTotals]: - async_add_entities([ Sensor( coordinator, device_info, - serial_number, - f"{serial_number}-import-energy-total", - f"Sems Total Import", - [GOODWE_SPELLING.energyStatisticsTotals, "buy"], + f"{serial_number}-{GOODWE_SPELLING.thisMonthTotalE}", + f"Inverter {inverter['name']} Energy This Month", + path_to_inverter + [GOODWE_SPELLING.thisMonthTotalE], Decimal, SensorDeviceClass.ENERGY, ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING + SensorStateClass.TOTAL_INCREASING, ), Sensor( coordinator, device_info, - serial_number, - f"{serial_number}-export-energy-total", - f"Sems Total Export", - [GOODWE_SPELLING.energyStatisticsTotals, "sell"], + f"{serial_number}-{GOODWE_SPELLING.lastMonthTotalE}", + f"Inverter {inverter['name']} Energy Last Month", + path_to_inverter + [GOODWE_SPELLING.lastMonthTotalE], Decimal, SensorDeviceClass.ENERGY, ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING + SensorStateClass.TOTAL_INCREASING, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-iday", + f"Inverter {inverter['name']} Income Today", + path_to_inverter + ["iday"], + Decimal, + SensorDeviceClass.MONETARY, + currency, + SensorStateClass.TOTAL, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-itotal", + f"Inverter {inverter['name']} Income Total", + path_to_inverter + ["itotal"], + Decimal, + SensorDeviceClass.MONETARY, + currency, + SensorStateClass.TOTAL, + ), + ] + + [ + Sensor( + coordinator, + device_info, + f"{serial_number}-vpv{idx}", + f"Inverter {inverter['name']} PV String {idx} Voltage", + path_to_inverter + [f"vpv{idx}"], + Decimal, + SensorDeviceClass.VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + ) + for idx in range(1, 5) + ] + + [ + Sensor( + coordinator, + device_info, + f"{serial_number}-ipv{idx}", + f"Inverter {inverter['name']} PV String {idx} Current", + path_to_inverter + [f"ipv{idx}"], + Decimal, + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + ) + for idx in range(1, 5) + ] + + [ + Sensor( + coordinator, + device_info, + f"{serial_number}-vac{idx}", + f"Inverter {inverter['name']} {idx} AC Voltage", + path_to_inverter + [f"vac{idx}"], + Decimal, + SensorDeviceClass.VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + AC_EMPTY, + ) + for idx in range(1, 4) + ] + + [ + Sensor( + coordinator, + device_info, + f"{serial_number}-iac{idx}", + f"Inverter {inverter['name']} Grid {idx} AC Current", + path_to_inverter + [f"iac{idx}"], + Decimal, + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + AC_CURRENT_EMPTY, + ) + for idx in range(1, 4) + ] + + [ + Sensor( + coordinator, + device_info, + f"{serial_number}-fac{idx}", + f"Inverter {inverter['name']} Grid {idx} AC Frequency", + path_to_inverter + [f"fac{idx}"], + Decimal, + SensorDeviceClass.FREQUENCY, + FREQUENCY_HERTZ, + SensorStateClass.MEASUREMENT, + AC_FEQ_EMPTY, + ) + for idx in range(1, 4) + ] + + [ + Sensor( + coordinator, + device_info, + f"{serial_number}-vbattery1", + f"Inverter {inverter['name']} Battery Voltage", + path_to_inverter + ["vbattery1"], + Decimal, + SensorDeviceClass.VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-ibattery1", + f"Inverter {inverter['name']} Battery Current", + path_to_inverter + ["ibattery1"], + Decimal, + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, ), - ]) + ] + ) + + if "hasPowerflow" in data and data["hasPowerflow"] and "powerflow" in data: + serial_number = data[GOODWE_SPELLING.homeKit]["sn"] + device_info = DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, serial_number) + }, + name="HomeKit", + manufacturer="GoodWe", + ) + async_add_entities( + [ + Sensor( + coordinator, + device_info, + f"{serial_number}", + f"HomeKit Load", + ["powerflow", "load"], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-pv", + f"HomeKit PV", + ["powerflow", "pv"], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-grid", + f"HomeKit Grid", + ["powerflow", "grid"], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-battery", + f"HomeKit Battery", + ["powerflow", GOODWE_SPELLING.battery], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-genset", + f"HomeKit generator", + ["powerflow", "genset"], + Decimal, + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-soc", + f"HomeKit State of Charge", + ["powerflow", "soc"], + Decimal, + SensorDeviceClass.BATTERY, + PERCENTAGE, + SensorStateClass.MEASUREMENT, + ), + ] + ) + if ( + GOODWE_SPELLING.hasEnergyStatisticsCharts in data + and data[GOODWE_SPELLING.hasEnergyStatisticsCharts] + ): + if data[GOODWE_SPELLING.energyStatisticsCharts]: + async_add_entities( + [ + Sensor( + coordinator, + device_info, + f"{serial_number}-import-energy", + f"Sems Import", + [GOODWE_SPELLING.energyStatisticsCharts, "buy"], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-export-energy", + f"Sems Export", + [GOODWE_SPELLING.energyStatisticsCharts, "sell"], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + ] + ) + if data[GOODWE_SPELLING.energyStatisticsTotals]: + async_add_entities( + [ + Sensor( + coordinator, + device_info, + f"{serial_number}-import-energy-total", + f"Sems Total Import", + [GOODWE_SPELLING.energyStatisticsTotals, "buy"], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + Sensor( + coordinator, + device_info, + f"{serial_number}-export-energy-total", + f"Sems Total Export", + [GOODWE_SPELLING.energyStatisticsTotals, "sell"], + Decimal, + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + ] + ) From a7ae2fbbc446fbbb66f035661a49d26fc7f618d2 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Sat, 21 Oct 2023 19:52:41 +1100 Subject: [PATCH 04/17] Compat --- custom_components/sems/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index cd12d81..131542c 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -180,7 +180,7 @@ async def async_update_data(): # Serial numbers are unique identifiers within a specific domain (DOMAIN, serial_number) }, - name=name, + name=f"Inverter {name}", manufacturer="GoodWe", model=device_data.get("model_type", "unknown"), sw_version=device_data.get("firmwareversion", "unknown"), @@ -201,7 +201,7 @@ async def async_update_data(): Sensor( coordinator, device_info, - serial_number, + serial_number, # backwards compatibility otherwise would be f"{serial_number}-power" f"Inverter {inverter['name']} Power", path_to_inverter + ["pac"], Decimal, @@ -397,10 +397,11 @@ async def async_update_data(): if "hasPowerflow" in data and data["hasPowerflow"] and "powerflow" in data: serial_number = data[GOODWE_SPELLING.homeKit]["sn"] + serial_backwards_compatibility = "homeKit" # the old code uses homeKit for the serial number device_info = DeviceInfo( identifiers={ # Serial numbers are unique identifiers within a specific domain - (DOMAIN, serial_number) + (DOMAIN, serial_backwards_compatibility) }, name="HomeKit", manufacturer="GoodWe", @@ -410,7 +411,7 @@ async def async_update_data(): Sensor( coordinator, device_info, - f"{serial_number}", + f"{serial_number}", # backwards compatibility otherwise would be f"{serial_number}-load" f"HomeKit Load", ["powerflow", "load"], Decimal, From fdf54fe91dc53bd0b873ea43114de0694a63df1f Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Sun, 22 Oct 2023 19:11:07 +1100 Subject: [PATCH 05/17] Fix typo in Grid ac voltage. sensor name. --- custom_components/sems/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 131542c..b52c418 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -329,7 +329,7 @@ async def async_update_data(): coordinator, device_info, f"{serial_number}-vac{idx}", - f"Inverter {inverter['name']} {idx} AC Voltage", + f"Inverter {inverter['name']} Grid {idx} AC Voltage", path_to_inverter + [f"vac{idx}"], Decimal, SensorDeviceClass.VOLTAGE, From e41941e92caa79dc0ee17307dbf530e05966a603 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Sat, 4 Nov 2023 16:09:09 +1100 Subject: [PATCH 06/17] Use v3 of the GetMonitorDetailByPowerstationId api for more decimal points. Update manifest.json with iot_class and integration_type. Change version to 4.0.0 --- custom_components/sems/manifest.json | 8 +++++--- custom_components/sems/sems_api.py | 5 +---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/custom_components/sems/manifest.json b/custom_components/sems/manifest.json index 32b3f79..fa6f90e 100644 --- a/custom_components/sems/manifest.json +++ b/custom_components/sems/manifest.json @@ -4,8 +4,10 @@ "config_flow": true, "documentation": "https://github.com/TimSoethout/goodwe-sems-home-assistant", "issue_tracker": "https://github.com/TimSoethout/goodwe-sems-home-assistant/issues", - "requirements": [], + "requirements": [], "dependencies": [], - "codeowners": ["@TimSoethout"], - "version": "3.7.2" + "codeowners": ["@TimSoethout", "@L-four"], + "iot_class": "cloud_polling", + "integration_type": "hub", + "version": "4.0.0" } diff --git a/custom_components/sems/sems_api.py b/custom_components/sems/sems_api.py index e181c95..533aa64 100644 --- a/custom_components/sems/sems_api.py +++ b/custom_components/sems/sems_api.py @@ -1,15 +1,12 @@ -import json import logging import requests - from homeassistant import exceptions _LOGGER = logging.getLogger(__name__) -# _LoginURL = "https://eu.semsportal.com/api/v2/Common/CrossLogin" _LoginURL = "https://www.semsportal.com/api/v2/Common/CrossLogin" -_PowerStationURLPart = "/v2/PowerStation/GetMonitorDetailByPowerstationId" +_PowerStationURLPart = "/v3/PowerStation/GetMonitorDetailByPowerstationId" _RequestTimeout = 30 # seconds _DefaultHeaders = { From 6c748ef27b7cbcc24b602a08b59c7dd9c926a4ed Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Sat, 4 Nov 2023 16:24:20 +1100 Subject: [PATCH 07/17] Refactor sensor instantiation --- custom_components/sems/sensor.py | 45 ++++++++------------------------ 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index b52c418..28d5dad 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -185,8 +185,7 @@ async def async_update_data(): model=device_data.get("model_type", "unknown"), sw_version=device_data.get("firmwareversion", "unknown"), ) - async_add_entities( - [ + sensors = [ Sensor( coordinator, device_info, @@ -296,7 +295,7 @@ async def async_update_data(): SensorStateClass.TOTAL, ), ] - + [ + inverter_strings = [ Sensor( coordinator, device_info, @@ -307,24 +306,8 @@ async def async_update_data(): SensorDeviceClass.VOLTAGE, ELECTRIC_POTENTIAL_VOLT, SensorStateClass.MEASUREMENT, - ) - for idx in range(1, 5) - ] - + [ - Sensor( - coordinator, - device_info, - f"{serial_number}-ipv{idx}", - f"Inverter {inverter['name']} PV String {idx} Current", - path_to_inverter + [f"ipv{idx}"], - Decimal, - SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, - SensorStateClass.MEASUREMENT, - ) - for idx in range(1, 5) - ] - + [ + ) for idx in range(1, 5)] + inverter_strings += [ Sensor( coordinator, device_info, @@ -336,10 +319,8 @@ async def async_update_data(): ELECTRIC_POTENTIAL_VOLT, SensorStateClass.MEASUREMENT, AC_EMPTY, - ) - for idx in range(1, 4) - ] - + [ + ) for idx in range(1, 4) ] + grids = [ Sensor( coordinator, device_info, @@ -351,10 +332,8 @@ async def async_update_data(): ELECTRIC_CURRENT_AMPERE, SensorStateClass.MEASUREMENT, AC_CURRENT_EMPTY, - ) - for idx in range(1, 4) - ] - + [ + ) for idx in range(1, 4) ] + grids += [ Sensor( coordinator, device_info, @@ -366,10 +345,8 @@ async def async_update_data(): FREQUENCY_HERTZ, SensorStateClass.MEASUREMENT, AC_FEQ_EMPTY, - ) - for idx in range(1, 4) - ] - + [ + ) for idx in range(1, 4)] + batteries = [ Sensor( coordinator, device_info, @@ -393,7 +370,7 @@ async def async_update_data(): SensorStateClass.MEASUREMENT, ), ] - ) + async_add_entities(sensors + inverter_strings + grids + batteries) if "hasPowerflow" in data and data["hasPowerflow"] and "powerflow" in data: serial_number = data[GOODWE_SPELLING.homeKit]["sn"] From 9add1d22e23cf1aa0620d3163a8a213f823d0435 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Sat, 4 Nov 2023 16:30:31 +1100 Subject: [PATCH 08/17] Fix manifest.json conflict --- custom_components/sems/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sems/manifest.json b/custom_components/sems/manifest.json index fa6f90e..9bc8eaf 100644 --- a/custom_components/sems/manifest.json +++ b/custom_components/sems/manifest.json @@ -4,10 +4,10 @@ "config_flow": true, "documentation": "https://github.com/TimSoethout/goodwe-sems-home-assistant", "issue_tracker": "https://github.com/TimSoethout/goodwe-sems-home-assistant/issues", + "integration_type": "hub", + "iot_class": "cloud_polling", "requirements": [], "dependencies": [], "codeowners": ["@TimSoethout", "@L-four"], - "iot_class": "cloud_polling", - "integration_type": "hub", "version": "4.0.0" } From 300d1b36ac751a8af17a9de97aad2b35a29cc7a7 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Sat, 4 Nov 2023 18:00:43 +1100 Subject: [PATCH 09/17] The json import is required. My IDE lied to me. --- custom_components/sems/sems_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sems/sems_api.py b/custom_components/sems/sems_api.py index 533aa64..98f92e1 100644 --- a/custom_components/sems/sems_api.py +++ b/custom_components/sems/sems_api.py @@ -1,5 +1,5 @@ import logging - +import json import requests from homeassistant import exceptions From 571df4b6695799096d61150ffb67d24eed5f2f58 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Thu, 9 Nov 2023 23:19:27 +1100 Subject: [PATCH 10/17] Refactor computing sensors to facilitate testing. Account for None path values or missing paths. Add some tests. --- custom_components/sems/sensor.py | 715 ++++++++++++----------- tests/data/test_data_001.py | 958 +++++++++++++++++++++++++++++++ tests/data/test_data_002.py | 569 ++++++++++++++++++ tests/test.py | 62 ++ 4 files changed, 1957 insertions(+), 347 deletions(-) create mode 100644 tests/data/test_data_001.py create mode 100644 tests/data/test_data_002.py create mode 100644 tests/test.py diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 28d5dad..60cf840 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -52,9 +52,15 @@ def get_value_from_path(data, path): + """ + Get value from a nested dictionary. + """ value = data - for key in path: - value = value[key] + try: + for key in path: + value = value[key] + except KeyError: + return None return value @@ -105,6 +111,349 @@ def suggested_display_precision(self): return 2 +class SensorOptions: + def __init__( + self, + device_info: DeviceInfo, + unique_id: str, + name: str, + value_path: List[str], + device_class: Optional[SensorDeviceClass] = None, + native_unit_of_measurement: Optional[str] = None, + state_class: Optional[SensorStateClass] = None, + empty_value=None, + data_type_converter=Decimal, + ): + self.device_info = device_info + self.unique_id = unique_id + self.name = name + self.value_path = value_path + self.device_class = device_class + self.native_unit_of_measurement = native_unit_of_measurement + self.state_class = state_class + self.empty_value = empty_value + self.data_type_converter = data_type_converter + + def __str__(self): + return f"SensorOptions(device_info={self.device_info}, unique_id={self.unique_id}, name={self.name}, value_path={self.value_path}, device_class={self.device_class}, native_unit_of_measurement={self.native_unit_of_measurement}, state_class={self.state_class}, empty_value={self.empty_value}, data_type_converter={self.data_type_converter})" + + +def sensor_options_for_data(data) -> List[SensorOptions]: + sensors: List[SensorOptions] = [] + try: + currency = data["kpi"]["currency"] + except KeyError: + currency = None + + for idx, inverter in enumerate(data["inverter"]): + serial_number = inverter["sn"] + path_to_inverter = ["inverter", idx, "invert_full"] + name = inverter.get("name", "unknown") + device_data = get_value_from_path(data, path_to_inverter) + device_info = DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, serial_number) + }, + name=f"Inverter {name}", + manufacturer="GoodWe", + model=device_data.get("model_type", "unknown"), + sw_version=device_data.get("firmwareversion", "unknown"), + ) + sensors += [ + SensorOptions( + device_info, + f"{serial_number}-capacity", + f"Inverter {inverter['name']} Capacity", + path_to_inverter + ["capacity"], + SensorDeviceClass.POWER, + POWER_KILO_WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + serial_number, # backwards compatibility otherwise would be f"{serial_number}-power" + f"Inverter {inverter['name']} Power", + path_to_inverter + ["pac"], + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-energy", + f"Inverter {inverter['name']} Energy", + path_to_inverter + ["etotal"], + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + SensorOptions( + device_info, + f"{serial_number}-hour-total", + f"Inverter {inverter['name']} Total Hours", + path_to_inverter + ["hour_total"], + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorOptions( + device_info, + f"{serial_number}-temperature", + f"Inverter {inverter['name']} Temperature", + path_to_inverter + [GOODWE_SPELLING.temperature], + SensorDeviceClass.TEMPERATURE, + TEMP_CELSIUS, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-eday ", + f"Inverter {inverter['name']} Energy Today", + path_to_inverter + ["eday"], + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + SensorOptions( + device_info, + f"{serial_number}-{GOODWE_SPELLING.thisMonthTotalE}", + f"Inverter {inverter['name']} Energy This Month", + path_to_inverter + [GOODWE_SPELLING.thisMonthTotalE], + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + SensorOptions( + device_info, + f"{serial_number}-{GOODWE_SPELLING.lastMonthTotalE}", + f"Inverter {inverter['name']} Energy Last Month", + path_to_inverter + [GOODWE_SPELLING.lastMonthTotalE], + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + SensorOptions( + device_info, + f"{serial_number}-iday", + f"Inverter {inverter['name']} Income Today", + path_to_inverter + ["iday"], + SensorDeviceClass.MONETARY, + currency, + SensorStateClass.TOTAL, + ), + SensorOptions( + device_info, + f"{serial_number}-itotal", + f"Inverter {inverter['name']} Income Total", + path_to_inverter + ["itotal"], + SensorDeviceClass.MONETARY, + currency, + SensorStateClass.TOTAL, + ), + ] + sensors += [ + SensorOptions( + device_info, + f"{serial_number}-vpv{idx}", + f"Inverter {inverter['name']} PV String {idx} Voltage", + path_to_inverter + [f"vpv{idx}"], + SensorDeviceClass.VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + ) + for idx in range(1, 5) + if get_value_from_path(data, path_to_inverter + [f"vpv{idx}"]) is not None + ] + sensors += [ + SensorOptions( + device_info, + f"{serial_number}-ipv{idx}", + f"Inverter {inverter['name']} PV String {idx} Current", + path_to_inverter + [f"ipv{idx}"], + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + ) + for idx in range(1, 5) + if get_value_from_path(data, path_to_inverter + [f"ipv{idx}"]) is not None + ] + sensors += [ + SensorOptions( + device_info, + f"{serial_number}-vac{idx}", + f"Inverter {inverter['name']} Grid {idx} AC Voltage", + path_to_inverter + [f"vac{idx}"], + SensorDeviceClass.VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + AC_EMPTY, + ) + for idx in range(1, 4) + ] + sensors += [ + SensorOptions( + device_info, + f"{serial_number}-iac{idx}", + f"Inverter {inverter['name']} Grid {idx} AC Current", + path_to_inverter + [f"iac{idx}"], + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + AC_CURRENT_EMPTY, + ) + for idx in range(1, 4) + ] + sensors += [ + SensorOptions( + device_info, + f"{serial_number}-fac{idx}", + f"Inverter {inverter['name']} Grid {idx} AC Frequency", + path_to_inverter + [f"fac{idx}"], + SensorDeviceClass.FREQUENCY, + FREQUENCY_HERTZ, + SensorStateClass.MEASUREMENT, + AC_FEQ_EMPTY, + ) + for idx in range(1, 4) + ] + sensors += [ + SensorOptions( + device_info, + f"{serial_number}-vbattery1", + f"Inverter {inverter['name']} Battery Voltage", + path_to_inverter + ["vbattery1"], + SensorDeviceClass.VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-ibattery1", + f"Inverter {inverter['name']} Battery Current", + path_to_inverter + ["ibattery1"], + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + ), + ] + + if "hasPowerflow" in data and data["hasPowerflow"] and "powerflow" in data: + serial_number = "powerflow" + serial_backwards_compatibility = ( + "homeKit" # the old code uses homeKit for the serial number + ) + device_info = DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, serial_backwards_compatibility) + }, + name="HomeKit", + manufacturer="GoodWe", + ) + sensors += [ + SensorOptions( + device_info, + f"{serial_number}", # backwards compatibility otherwise would be f"{serial_number}-load" + f"HomeKit Load", + ["powerflow", "load"], + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-pv", + f"HomeKit PV", + ["powerflow", "pv"], + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-grid", + f"HomeKit Grid", + ["powerflow", "grid"], + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-battery", + f"HomeKit Battery", + ["powerflow", GOODWE_SPELLING.battery], + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-genset", + f"HomeKit generator", + ["powerflow", "genset"], + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-soc", + f"HomeKit State of Charge", + ["powerflow", "soc"], + SensorDeviceClass.BATTERY, + PERCENTAGE, + SensorStateClass.MEASUREMENT, + ), + ] + if ( + GOODWE_SPELLING.hasEnergyStatisticsCharts in data + and data[GOODWE_SPELLING.hasEnergyStatisticsCharts] + ): + if data[GOODWE_SPELLING.energyStatisticsCharts]: + sensors += [ + SensorOptions( + device_info, + f"{serial_number}-import-energy", + f"Sems Import", + [GOODWE_SPELLING.energyStatisticsCharts, "buy"], + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + SensorOptions( + device_info, + f"{serial_number}-export-energy", + f"Sems Export", + [GOODWE_SPELLING.energyStatisticsCharts, "sell"], + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + ] + if data[GOODWE_SPELLING.energyStatisticsTotals]: + sensors += [ + SensorOptions( + device_info, + f"{serial_number}-import-energy-total", + f"Sems Total Import", + [GOODWE_SPELLING.energyStatisticsTotals, "buy"], + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + SensorOptions( + device_info, + f"{serial_number}-export-energy-total", + f"Sems Total Export", + [GOODWE_SPELLING.energyStatisticsTotals, "sell"], + SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + ] + return sensors + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -165,349 +514,21 @@ async def async_update_data(): data = coordinator.data - try: - currency = data["kpi"]["currency"] - except KeyError: - currency = None - - for idx, inverter in enumerate(data["inverter"]): - serial_number = inverter["sn"] - path_to_inverter = ["inverter", idx, "invert_full"] - name = inverter.get("name", "unknown") - device_data = get_value_from_path(coordinator.data, path_to_inverter) - device_info = DeviceInfo( - identifiers={ - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, serial_number) - }, - name=f"Inverter {name}", - manufacturer="GoodWe", - model=device_data.get("model_type", "unknown"), - sw_version=device_data.get("firmwareversion", "unknown"), - ) - sensors = [ - Sensor( - coordinator, - device_info, - f"{serial_number}-capacity", - f"Inverter {inverter['name']} Capacity", - path_to_inverter + ["capacity"], - Decimal, - SensorDeviceClass.POWER, - POWER_KILO_WATT, - SensorStateClass.MEASUREMENT, - ), - Sensor( - coordinator, - device_info, - serial_number, # backwards compatibility otherwise would be f"{serial_number}-power" - f"Inverter {inverter['name']} Power", - path_to_inverter + ["pac"], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-energy", - f"Inverter {inverter['name']} Energy", - path_to_inverter + ["etotal"], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-hour-total", - f"Inverter {inverter['name']} Total Hours", - path_to_inverter + ["hour_total"], - Decimal, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-temperature", - f"Inverter {inverter['name']} Temperature", - path_to_inverter + [GOODWE_SPELLING.temperature], - Decimal, - SensorDeviceClass.TEMPERATURE, - TEMP_CELSIUS, - SensorStateClass.MEASUREMENT, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-eday ", - f"Inverter {inverter['name']} Energy Today", - path_to_inverter + ["eday"], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-{GOODWE_SPELLING.thisMonthTotalE}", - f"Inverter {inverter['name']} Energy This Month", - path_to_inverter + [GOODWE_SPELLING.thisMonthTotalE], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-{GOODWE_SPELLING.lastMonthTotalE}", - f"Inverter {inverter['name']} Energy Last Month", - path_to_inverter + [GOODWE_SPELLING.lastMonthTotalE], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-iday", - f"Inverter {inverter['name']} Income Today", - path_to_inverter + ["iday"], - Decimal, - SensorDeviceClass.MONETARY, - currency, - SensorStateClass.TOTAL, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-itotal", - f"Inverter {inverter['name']} Income Total", - path_to_inverter + ["itotal"], - Decimal, - SensorDeviceClass.MONETARY, - currency, - SensorStateClass.TOTAL, - ), - ] - inverter_strings = [ - Sensor( - coordinator, - device_info, - f"{serial_number}-vpv{idx}", - f"Inverter {inverter['name']} PV String {idx} Voltage", - path_to_inverter + [f"vpv{idx}"], - Decimal, - SensorDeviceClass.VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, - SensorStateClass.MEASUREMENT, - ) for idx in range(1, 5)] - inverter_strings += [ - Sensor( - coordinator, - device_info, - f"{serial_number}-vac{idx}", - f"Inverter {inverter['name']} Grid {idx} AC Voltage", - path_to_inverter + [f"vac{idx}"], - Decimal, - SensorDeviceClass.VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, - SensorStateClass.MEASUREMENT, - AC_EMPTY, - ) for idx in range(1, 4) ] - grids = [ - Sensor( - coordinator, - device_info, - f"{serial_number}-iac{idx}", - f"Inverter {inverter['name']} Grid {idx} AC Current", - path_to_inverter + [f"iac{idx}"], - Decimal, - SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, - SensorStateClass.MEASUREMENT, - AC_CURRENT_EMPTY, - ) for idx in range(1, 4) ] - grids += [ - Sensor( - coordinator, - device_info, - f"{serial_number}-fac{idx}", - f"Inverter {inverter['name']} Grid {idx} AC Frequency", - path_to_inverter + [f"fac{idx}"], - Decimal, - SensorDeviceClass.FREQUENCY, - FREQUENCY_HERTZ, - SensorStateClass.MEASUREMENT, - AC_FEQ_EMPTY, - ) for idx in range(1, 4)] - batteries = [ - Sensor( - coordinator, - device_info, - f"{serial_number}-vbattery1", - f"Inverter {inverter['name']} Battery Voltage", - path_to_inverter + ["vbattery1"], - Decimal, - SensorDeviceClass.VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, - SensorStateClass.MEASUREMENT, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-ibattery1", - f"Inverter {inverter['name']} Battery Current", - path_to_inverter + ["ibattery1"], - Decimal, - SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, - SensorStateClass.MEASUREMENT, - ), - ] - async_add_entities(sensors + inverter_strings + grids + batteries) - - if "hasPowerflow" in data and data["hasPowerflow"] and "powerflow" in data: - serial_number = data[GOODWE_SPELLING.homeKit]["sn"] - serial_backwards_compatibility = "homeKit" # the old code uses homeKit for the serial number - device_info = DeviceInfo( - identifiers={ - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, serial_backwards_compatibility) - }, - name="HomeKit", - manufacturer="GoodWe", - ) - async_add_entities( - [ - Sensor( - coordinator, - device_info, - f"{serial_number}", # backwards compatibility otherwise would be f"{serial_number}-load" - f"HomeKit Load", - ["powerflow", "load"], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-pv", - f"HomeKit PV", - ["powerflow", "pv"], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-grid", - f"HomeKit Grid", - ["powerflow", "grid"], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-battery", - f"HomeKit Battery", - ["powerflow", GOODWE_SPELLING.battery], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-genset", - f"HomeKit generator", - ["powerflow", "genset"], - Decimal, - SensorDeviceClass.POWER, - POWER_WATT, - SensorStateClass.MEASUREMENT, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-soc", - f"HomeKit State of Charge", - ["powerflow", "soc"], - Decimal, - SensorDeviceClass.BATTERY, - PERCENTAGE, - SensorStateClass.MEASUREMENT, - ), - ] + sensor_options: List[SensorOptions] = sensor_options_for_data(data) + sensors: List[Sensor] = [] + for sensor_option in sensor_options: + sensors.append( + Sensor( + coordinator, + sensor_option.device_info, + sensor_option.unique_id, + sensor_option.name, + sensor_option.value_path, + sensor_option.data_type_converter, + sensor_option.device_class, + sensor_option.native_unit_of_measurement, + sensor_option.state_class, + sensor_option.empty_value, + ) ) - if ( - GOODWE_SPELLING.hasEnergyStatisticsCharts in data - and data[GOODWE_SPELLING.hasEnergyStatisticsCharts] - ): - if data[GOODWE_SPELLING.energyStatisticsCharts]: - async_add_entities( - [ - Sensor( - coordinator, - device_info, - f"{serial_number}-import-energy", - f"Sems Import", - [GOODWE_SPELLING.energyStatisticsCharts, "buy"], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-export-energy", - f"Sems Export", - [GOODWE_SPELLING.energyStatisticsCharts, "sell"], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING, - ), - ] - ) - if data[GOODWE_SPELLING.energyStatisticsTotals]: - async_add_entities( - [ - Sensor( - coordinator, - device_info, - f"{serial_number}-import-energy-total", - f"Sems Total Import", - [GOODWE_SPELLING.energyStatisticsTotals, "buy"], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING, - ), - Sensor( - coordinator, - device_info, - f"{serial_number}-export-energy-total", - f"Sems Total Export", - [GOODWE_SPELLING.energyStatisticsTotals, "sell"], - Decimal, - SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, - SensorStateClass.TOTAL_INCREASING, - ), - ] - ) + async_add_entities(sensors) diff --git a/tests/data/test_data_001.py b/tests/data/test_data_001.py new file mode 100644 index 0000000..260b798 --- /dev/null +++ b/tests/data/test_data_001.py @@ -0,0 +1,958 @@ +data_001 = { + "info": { + "time": "11/07/2023 06: 02: 28", + }, + "kpi": { + "month_generation": 1234, + "pac": 0.0, + "power": 0.0, + "total_power": 1234, + "day_income": 0.0, + "total_income": 1234, + }, + "powercontrol_status": 0, + "images": [], + "weather": { + + }, + "inverter": [ + { + "sn": "the_serial_number", + "is_stored": True, + "name": "the_inverter_model", + "in_pac": 0.0, + "out_pac": 0.0, + "eday": 0.0, + "emonth": 1234, + "etotal": 1234, + "status": 1, + "turnon_time": "02/07/2023 05: 19: 21", + "releation_id": "a_uid", + "type": "the_inverter_model", + "capacity": 10.0, + "d": { + "pw_id": "a_uid", + "capacity": "1234kW", + "model": "the_inverter_model", + "output_power": "36W", + "output_current": "0.9A", + "grid_voltage": "233.1V/233.8V/233.2V", + "backup_output": "22.3V/0W", + "soc": "5%", + "soh": "100%", + "last_refresh_time": "11.07.2023 06: 00: 09", + "work_mode": "Wait Mode", + "dc_input1": "0V/0A", + "dc_input2": "0V/0A", + "battery": "205.1V/0A/0W", + "bms_status": "StandbyOfBattery", + "warning": "Normal", + "charge_current_limit": "40A", + "discharge_current_limit": "40A", + "firmware_version": 1234.0, + "creationDate": "11/07/2023 13: 00: 09", + "eDay": 0.0, + "eTotal": 1234, + "pac": 0.0, + "hTotal": 1234, + "vpv1": 0.0, + "vpv2": 0.0, + "vpv3": None, + "vpv4": None, + "ipv1": 0.0, + "ipv2": 0.0, + "ipv3": None, + "ipv4": None, + "vac1": 233.1, + "vac2": 233.8, + "vac3": 233.2, + "iac1": 0.9, + "iac2": 0.9, + "iac3": 0.8, + "fac1": 50.01, + "fac2": 50.04, + "fac3": 50.03, + "istr1": 0.0, + "istr2": 0.0, + "istr3": None, + "istr4": None, + "istr5": None, + "istr6": 17.0, + "istr7": None, + "istr8": 7.0, + "istr9": None, + "istr10": 11.0, + "istr11": None, + "istr12": 0.0, + "istr13": 0.0, + "istr14": 0.0, + "istr15": 0.0, + "istr16": 0.0 + }, + "it_change_flag": False, + "tempperature": 35.1, + "check_code": "1234", + "next": None, + "prev": None, + "next_device": { + "sn": None, + "isStored": False + }, + "prev_device": { + "sn": None, + "isStored": False + }, + "invert_full": { + "ct_solution_type": 0, + "cts": None, + "sn": "the_serial_number", + "check_code": "1234", + "powerstation_id": "a_uid", + "name": "the_inverter_model", + "model_type": "the_inverter_model", + "change_type": 0, + "change_time": 0, + "capacity": 1234.0, + "eday": 0.0, + "iday": 1234, + "etotal": 1234, + "itotal": 1234, + "hour_total": 1234, + "status": 1, + "turnon_time": 1234, + "pac": 0.0, + "tempperature": 35.1, + "vpv1": 0.0, + "vpv2": 0.0, + "vpv3": None, + "vpv4": None, + "ipv1": 0.0, + "ipv2": 0.0, + "ipv3": None, + "ipv4": None, + "vac1": 233.1, + "vac2": 233.8, + "vac3": 233.2, + "iac1": 0.9, + "iac2": 0.9, + "iac3": 0.8, + "fac1": 50.01, + "fac2": 50.04, + "fac3": 50.03, + "istr1": 0.0, + "istr2": 0.0, + "istr3": None, + "istr4": None, + "istr5": None, + "istr6": 17.0, + "istr7": None, + "istr8": 7.0, + "istr9": None, + "istr10": 11.0, + "istr11": None, + "istr12": 0.0, + "istr13": 0.0, + "istr14": 0.0, + "istr15": 0.0, + "istr16": 0.0, + "last_time": 1234, + "vbattery1": 205.1, + "ibattery1": 0.0, + "pmeter": -1234, + "soc": 5.0, + "soh": 100.0, + "bms_discharge_i_max": 40.0, + "bms_charge_i_max": 40.0, + "bms_warning": 0, + "bms_alarm": 0, + "battary_work_mode": 1, + "workmode": 1, + "vload": 22.3, + "iload": 0.0, + "firmwareversion": 10.0, + "bmssoftwareversion": 0.0, + "pbackup": 0.0, + "seller": 1234, + "buy": 1234, + "yesterdaybuytotal": 1234, + "yesterdaysellertotal": 1234, + "yesterdayct2sellertotal": None, + "yesterdayetotal": 1234, + "yesterdayetotalload": 1234, + "yesterdaylastime": 0, + "thismonthetotle": 1234, + "lastmonthetotle": 1234, + "ram": 26.1234, + "outputpower": 36.0, + "fault_messge": 0, + "battery1sn": None, + "battery2sn": None, + "battery3sn": None, + "battery4sn": None, + "battery5sn": None, + "battery6sn": None, + "battery7sn": None, + "battery8sn": None, + "pf": 0.0, + "pv_power": 0.0, + "reactive_power": 0.0, + "leakage_current": None, + "isoLimit": None, + "isbuettey": True, + "isbuetteybps": False, + "isbuetteybpu": False, + "isESUOREMU": False, + "backUpPload_S": 0.0, + "backUpVload_S": 22.8, + "backUpIload_S": 0.0, + "backUpPload_T": 0.0, + "backUpVload_T": 22.5, + "backUpIload_T": 0.0, + "eTotalBuy": 1234, + "eDayBuy": 0.0, + "eBatteryCharge": 1234, + "eChargeDay": 1234, + "eBatteryDischarge": 1234, + "eDischargeDay": 1234, + "battStrings": 0.0, + "meterConnectStatus": 1234, + "mtActivepowerR": -536.0, + "mtActivepowerS": -52.0, + "mtActivepowerT": -108.0, + "ezPro_connect_status": None, + "dataloggersn": "", + "equipment_name": None, + "hasmeter": True, + "meter_type": 1.0, + "pre_hour_lasttotal": None, + "pre_hour_time": None, + "current_hour_pv": 0.0, + "extend_properties": None, + "eP_connect_status_happen": None, + "eP_connect_status_recover": None, + "total_sell": 1234, + "total_buy": 1234, + "errors": [], + "safetyConutry": 2.0, + "deratingMode": None, + "master": None, + "parallel_code": None, + "inverter_type": 1, + "total_pbattery": 0.0, + "battery_count": 1, + "more_batterys": [ + { + "pbattery": 0.0, + "vbattery": 205.1, + "ibattery": 0.0, + "battary_work_mode": 1, + "battstrings": 4.0, + "bms_status": 1, + "bms_temperature": 23.0, + "bms_discharge_i_max": 40.0, + "bms_charge_i_max": 40.0, + "bms_alarm_l": 0, + "bms_warning_l": 0, + "soc": 5.0, + "soh": 100.0, + "bmsbattstr": 4, + "batteryprotocol": 1234, + "bms_alarm_h": 0, + "bms_warning_h": 0, + "bmssoftwareversion": 0.0, + "batteryhardwareversion": 0.0, + "batttotalcharge": 0.0, + "batttotaldisCharge": 0.0, + "batterysnmain": "", + "bms_alarm": 0, + "bms_warning": 0 + } + ], + "genset_start_mode": None, + "genset_mode": None, + "genset_etotal": None, + "genset_yeasterdayTotal": None, + "genset_power": None, + "genset_eday": 0.0, + "all_eday": 0.0, + "output_etotal": 1234, + "output_yeasterdayTotal": 1234, + "output_eday": 1234, + "battery_index": 1234 + }, + "time": "11/07/2023 06: 02: 28", + "battery": "205.1V/0A/0W", + "firmware_version": 1234, + "warning_bms": "Normal", + "soh": "100%", + "discharge_current_limit_bms": "40A", + "charge_current_limit_bms": "40A", + "soc": "5%", + "pv_input_2": "0V/0A", + "pv_input_1": "0V/0A", + "back_up_output": "22.3V/0W", + "output_voltage": "233.1V/233.8V/233.2V", + "backup_voltage": "22.3V/22.8V/22.5V", + "output_current": "0.9A", + "output_power": "36W", + "total_generation": "1234kWh", + "daily_generation": "1234", + "battery_charging": "205.1234A/0W", + "last_refresh_time": "11/07/2023 06: 00: 09", + "bms_status": "1234", + "pw_id": "a_uid", + "fault_message": "", + "warning_code": None, + "battery_power": 0.0, + "point_index": "12", + "points": [ + { + "target_index": 1, + "target_name": "Vpv1", + "display": "Vpv1(V)", + "unit": "V", + "target_key": "Vpv1", + "text_cn": "直流电压1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 2, + "target_name": "Vpv2", + "display": "Vpv2(V)", + "unit": "V", + "target_key": "Vpv2", + "text_cn": "直流电压2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 5, + "target_name": "Ipv1", + "display": "Ipv1(A)", + "unit": "A", + "target_key": "Ipv1", + "text_cn": "直流电流1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 6, + "target_name": "Ipv2", + "display": "Ipv2(A)", + "unit": "A", + "target_key": "Ipv2", + "text_cn": "直流电流2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 9, + "target_name": "Ua", + "display": "Ua(V)", + "unit": "V", + "target_key": "Vac1", + "text_cn": "交流电压1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 10, + "target_name": "Ub", + "display": "Ub(V)", + "unit": "V", + "target_key": "Vac2", + "text_cn": "交流电压2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 11, + "target_name": "Uc", + "display": "Uc(V)", + "unit": "V", + "target_key": "Vac3", + "text_cn": "交流电压3", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 12, + "target_name": "Iac1", + "display": "Iac1(A)", + "unit": "A", + "target_key": "Iac1", + "text_cn": "交流电流1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 13, + "target_name": "Iac2", + "display": "Iac2(A)", + "unit": "A", + "target_key": "Iac2", + "text_cn": "交流电流2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 14, + "target_name": "Iac3", + "display": "Iac3(A)", + "unit": "A", + "target_key": "Iac3", + "text_cn": "交流电流3", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 15, + "target_name": "Fac1", + "display": "Fac1(Hz)", + "unit": "Hz", + "target_key": "Fac1", + "text_cn": "频率1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 16, + "target_name": "Fac2", + "display": "Fac2(Hz)", + "unit": "Hz", + "target_key": "Fac2", + "text_cn": "频率2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 17, + "target_name": "Fac3", + "display": "Fac3(Hz)", + "unit": "Hz", + "target_key": "Fac3", + "text_cn": "频率3", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 18, + "target_name": "Power", + "display": "Power(W)", + "unit": "W", + "target_key": "Pac", + "text_cn": "功率", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 19, + "target_name": "WorkMode", + "display": "WorkMode", + "unit": "", + "target_key": "WorkMode", + "text_cn": "工作模式", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 20, + "target_name": "Temperature", + "display": "Temperature(℃)", + "unit": "℃", + "target_key": "Tempperature", + "text_cn": "温度", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 23, + "target_name": "HTotal", + "display": "HTotal(h)", + "unit": "h", + "target_key": "HTotal", + "text_cn": "工作时长", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 37, + "target_name": "PV Generation", + "display": "PV Generation(kWh)", + "unit": "kWh", + "target_key": "pv_etotal_l+pv_etotal_h*65536", + "text_cn": "光伏发电量", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 38, + "target_name": "Pmeter", + "display": "Pmeter(W)", + "unit": "W", + "target_key": "Pmeter", + "text_cn": "电表功率", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 39, + "target_name": "Pbackup", + "display": "Pbackup(W)", + "unit": "W", + "target_key": "Pbackup", + "text_cn": "后备输出功率", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 40, + "target_name": "Vbackup", + "display": "Vbackup(V)", + "unit": "V", + "target_key": "Vload", + "text_cn": "后备输出电压1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 41, + "target_name": "Ibackup", + "display": "Ibackup(A)", + "unit": "A", + "target_key": "Iload", + "text_cn": "后备输出电流1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 42, + "target_name": "Vbat", + "display": "Vbat(V)", + "unit": "V", + "target_key": "VBattery1", + "text_cn": "电池电压", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 43, + "target_name": "Ibat", + "display": "Ibat(A)", + "unit": "A", + "target_key": "IBattery1", + "text_cn": "电池电流", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 44, + "target_name": "SOC", + "display": "SOC(%)", + "unit": "%", + "target_key": "Cbattery1", + "text_cn": "电池电量", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 45, + "target_name": "SOH", + "display": "SOH(%)", + "unit": "%", + "target_key": "SOH", + "text_cn": "电池健康度", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 46, + "target_name": "BMS_Temperature", + "display": "BMS_Temperature(℃)", + "unit": "℃", + "target_key": "BMS_Temperature", + "text_cn": "电池温度", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 47, + "target_name": "BMS_Charge_I_Max", + "display": "BMS_Charge_I_Max(A)", + "unit": "A", + "target_key": "BMS_Charge_I_Max", + "text_cn": "电池充电最大电流", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 48, + "target_name": "BMS_Discharge_I_Max", + "display": "BMS_Discharge_I_Max(A)", + "unit": "A", + "target_key": "BMS_Discharge_I_Max", + "text_cn": "电池放电最大电流", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 50, + "target_name": "Total Output", + "display": "Total Output(kWh)", + "unit": "kWh", + "target_key": "ETotal_EXP_1", + "text_cn": "总输出电量", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 51, + "target_name": "Pbackup1", + "display": "Pbackup1(W)", + "unit": "W", + "target_key": "BackUpPload_R", + "text_cn": "后备输出功率1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 52, + "target_name": "Pbackup2", + "display": "Pbackup2(W)", + "unit": "W", + "target_key": "BackUpPload_S", + "text_cn": "后备输出功率2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 53, + "target_name": "Vbackup2", + "display": "Vbackup2(V)", + "unit": "V", + "target_key": "Istr18_EXP_1", + "text_cn": "后备输出电压2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 54, + "target_name": "Ibackup2", + "display": "Ibackup2(A)", + "unit": "A", + "target_key": "BackUpIload_S", + "text_cn": "后备输出电流2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 55, + "target_name": "Pbackup3", + "display": "Pbackup3(W)", + "unit": "W", + "target_key": "BackUpPload_T", + "text_cn": "后备输出功率3", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 56, + "target_name": "Vbackup3", + "display": "Vbackup3(V)", + "unit": "V", + "target_key": "BackUpVload_T", + "text_cn": "后备输出电压3", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 57, + "target_name": "Ibackup3", + "display": "Ibackup3(A)", + "unit": "A", + "target_key": "BackUpIload_T", + "text_cn": "后备输出电流3", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 58, + "target_name": "meterPhase1", + "display": "meterPhase1(W)", + "unit": "W", + "target_key": "MTActivepowerR", + "text_cn": "电表功率1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 59, + "target_name": "meterPhase2", + "display": "meterPhase2(W)", + "unit": "W", + "target_key": "MTActivepowerS", + "text_cn": "电表功率2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 60, + "target_name": "meterPhase3", + "display": "meterPhase3(W)", + "unit": "W", + "target_key": "MTActivepowerT", + "text_cn": "电表功率3", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 400, + "target_name": "BMS Version", + "display": "BMS Version", + "unit": "", + "target_key": "BMSSoftwareVersion", + "text_cn": "BMS版本", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 409, + "target_name": "e_charge", + "display": "e_charge(kWh)", + "unit": "kWh", + "target_key": "EBatteryCharge", + "text_cn": "电池充电", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + }, + { + "target_index": 410, + "target_name": "e_discharge", + "display": "e_discharge(kWh)", + "unit": "kWh", + "target_key": "EBatteryDischarge", + "text_cn": "电池放电", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None + } + ], + "backup_pload_s": 0.0, + "backup_vload_s": 22.8, + "backup_iload_s": 0.0, + "backup_pload_t": 0.0, + "backup_vload_t": 22.5, + "backup_iload_t": 0.0, + "etotal_buy": 1234, + "eday_buy": 0.0, + "ebattery_charge": 1234, + "echarge_day": 1234, + "ebattery_discharge": 1234, + "edischarge_day": 1234, + "batt_strings": 0.0, + "meter_connect_status": 1234, + "mtactivepower_r": -536.0, + "mtactivepower_s": -52.0, + "mtactivepower_t": -108.0, + "has_tigo": False, + "canStartIV": False, + "battery_count": 1 + } + ], + "hjgx": { + "co2": 1234, + "tree": 1234, + "coal": 1234 + }, + "homKit": { + "homeKitLimit": False, + "sn": None + }, + "isTigo": False, + "tigoIntervalTimeMinute": 15, + "smuggleInfo": { + "isAllSmuggle": False, + "isSmuggle": False, + "descriptionText": None, + "sns": None + }, + "hasPowerflow": True, + "powerflow": { + "pv": "0(W)", + "pvStatus": 0, + "bettery": "0(W)", + "betteryStatus": 0, + "betteryStatusStr": None, + "load": "1234(W)", + "loadStatus": 1, + "grid": "1234(W)", + "soc": 5, + "socText": "5%", + "hasEquipment": True, + "gridStatus": 1, + "isHomKit": False, + "isBpuAndInverterNoBattery": False, + "isMoreBettery": False, + "genset": "0(W)", + "gensetStatus": 0, + "gridGensetStatus": 0 + }, + "hasGridLoad": False, + "isParallelInventers": False, + "isEvCharge": False, + "evCharge": None, + "hasEnergeStatisticsCharts": True, + "energeStatisticsCharts": { + "contributingRate": 0.1234, + "selfUseRate": 0.0, + "sum": 0.0, + "buy": 1234, + "buyPercent": 1234, + "sell": 0.1234, + "sellPercent": 1234, + "selfUseOfPv": 1234, + "consumptionOfLoad": 1234, + "chartsType": 1, + "hasPv": True, + "hasCharge": False, + "charge": 0.0, + "disCharge": 0.0, + "gensetGen": 0.0, + "hasGenset": False + }, + "energeStatisticsTotals": { + "contributingRate": 1234, + "selfUseRate": 1234, + "sum": 1234, + "buy": 1234, + "buyPercent": 1234, + "sell": 1234, + "sellPercent": 1234, + "selfUseOfPv": 1234, + "consumptionOfLoad": 1234, + "chartsType": 1, + "hasPv": True, + "hasCharge": False, + "charge": 0.0, + "disCharge": 0.0, + "gensetGen": 0.0, + "hasGenset": False + }, + "soc": { + "power": 5, + "status": 0 + }, + "environmental": [], + "equipment": [ + { + "type": "5", + "title": "the_inverter_model", + "status": 1, + "model": None, + "statusText": None, + "capacity": None, + "actionThreshold": None, + "subordinateEquipment": "", + "powerGeneration": "PV: 0(kW)", + "eday": "Generation Today: 0(kWh)", + "brand": "", + "isStored": True, + "soc": "SOC: 5%", + "isChange": False, + "relationId": "a_uid", + "sn": "the_serial_number", + "has_tigo": False, + "is_sec": False, + "is_secs": False, + "targetPF": None, + "exportPowerlimit": None, + "titleSn": None + } + ] +} \ No newline at end of file diff --git a/tests/data/test_data_002.py b/tests/data/test_data_002.py new file mode 100644 index 0000000..f5a1722 --- /dev/null +++ b/tests/data/test_data_002.py @@ -0,0 +1,569 @@ +data_002 = { + "info": {}, + "kpi": { + "month_generation": 591.15, + "pac": 0, + "power": 32.19, + "total_power": 19815.45, + "day_income": 3.15, + "total_income": 1941.91, + "yield_rate": 0.098, + "currency": "AUD", + }, + "powercontrol_status": 0, + "images": [], + "weather": {}, + "inverter": [ + { + "sn": "FAKE_SN", + "is_stored": False, + "name": "House", + "in_pac": 0, + "out_pac": 0, + "eday": 32.2, + "emonth": 572.5, + "etotal": 23920.7, + "status": -1, + "turnon_time": "10/14/2020 09:13:30", + "releation_id": "edd93607-a44d-4647-86e5-afc5168d76a4", + "type": "model", + "capacity": 5, + "d": { + "pw_id": "ed5e828b-714c-446e-910e-864aa6bd6e8f", + "capacity": "5kW", + "model": "model", + "output_power": "0W", + "output_current": "0A", + "grid_voltage": "0V", + "backup_output": "0V/0W", + "soc": "519%", + "soh": "0%", + "last_refresh_time": "24/10/2023 20:12:49", + "work_mode": "Wait Mode", + "dc_input1": "0V/0A", + "dc_input2": "0V/0A", + "battery": "0V/0A/0W", + "bms_status": "", + "warning": "--", + "charge_current_limit": "0A", + "discharge_current_limit": "0A", + "firmware_version": 191917, + "creationDate": "10/24/2023 17:12:49", + "eDay": 32.2, + "eTotal": 23920.7, + "pac": 0, + "hTotal": 13351, + "vpv1": 0, + "vpv2": 0, + "vpv3": 0, + "vpv4": 0, + "ipv1": 0, + "ipv2": 0, + "ipv3": 0, + "ipv4": 0, + "vac1": 0, + "vac2": 0, + "vac3": 0, + "iac1": 0, + "iac2": 0, + "iac3": 0, + "fac1": 0, + "fac2": 0, + "fac3": 0, + "istr1": 0, + "istr2": 0, + "istr3": 0, + "istr4": 0, + "istr5": 0, + "istr6": 0, + "istr7": 0, + "istr8": 0, + "istr9": 0, + "istr10": 0, + "istr11": 0, + "istr12": 0, + "istr13": 0, + "istr14": 0, + "istr15": 0, + "istr16": 0, + }, + "it_change_flag": False, + "tempperature": 0, + "check_code": "047627", + "next": None, + "prev": None, + "next_device": {"sn": None, "isStored": False}, + "prev_device": {"sn": None, "isStored": False}, + "invert_full": { + "ct_solution_type": 0, + "cts": None, + "sn": "FAKE_SN", + "check_code": "047627", + "powerstation_id": "ed5e828b-714c-446e-910e-864aa6bd6e8f", + "name": "House", + "model_type": "model", + "change_type": 0, + "change_time": 0, + "capacity": 5, + "eday": 32.2, + "iday": 3.0184, + "etotal": 23920.7, + "itotal": 2344.2286000000004, + "hour_total": 13351, + "status": -1, + "turnon_time": 1602638010057, + "pac": 0, + "tempperature": 0, + "vpv1": 0, + "vpv2": 0, + "vpv3": 0, + "vpv4": 0, + "ipv1": 0, + "ipv2": 0, + "ipv3": 0, + "ipv4": 0, + "vac1": 0, + "vac2": 0, + "vac3": 0, + "iac1": 0, + "iac2": 0, + "iac3": 0, + "fac1": 0, + "fac2": 0, + "fac3": 0, + "istr1": 0, + "istr2": 0, + "istr3": 0, + "istr4": 0, + "istr5": 0, + "istr6": 0, + "istr7": 0, + "istr8": 0, + "istr9": 0, + "istr10": 0, + "istr11": 0, + "istr12": 0, + "istr13": 0, + "istr14": 0, + "istr15": 0, + "istr16": 0, + "last_time": 1698138769641, + "vbattery1": 0, + "ibattery1": 0, + "pmeter": 0, + "soc": 519, + "soh": -0.100000000000364, + "bms_discharge_i_max": None, + "bms_charge_i_max": 0, + "bms_warning": 0, + "bms_alarm": 65535, + "battary_work_mode": 0, + "workmode": 1, + "vload": 0, + "iload": 0, + "firmwareversion": 1919, + "bmssoftwareversion": None, + "pbackup": 0, + "seller": 0, + "buy": 0, + "yesterdaybuytotal": 0, + "yesterdaysellertotal": 0, + "yesterdayct2sellertotal": None, + "yesterdayetotal": None, + "yesterdayetotalload": 6553.5, + "yesterdaylastime": 0, + "thismonthetotle": 572.5, + "lastmonthetotle": 23316, + "ram": 17, + "outputpower": 0, + "fault_messge": 0, + "battery1sn": None, + "battery2sn": None, + "battery3sn": None, + "battery4sn": None, + "battery5sn": None, + "battery6sn": None, + "battery7sn": None, + "battery8sn": None, + "pf": -0.001, + "pv_power": 0, + "reactive_power": 0, + "leakage_current": 0, + "isoLimit": 65535, + "isbuettey": False, + "isbuetteybps": False, + "isbuetteybpu": False, + "isESUOREMU": False, + "backUpPload_S": 0, + "backUpVload_S": 0, + "backUpIload_S": 0, + "backUpPload_T": 0, + "backUpVload_T": 0, + "backUpIload_T": 0, + "eTotalBuy": None, + "eDayBuy": 0, + "eBatteryCharge": None, + "eChargeDay": 0, + "eBatteryDischarge": None, + "eDischargeDay": 0, + "battStrings": 6553.5, + "meterConnectStatus": None, + "mtActivepowerR": 0, + "mtActivepowerS": 0, + "mtActivepowerT": 0, + "ezPro_connect_status": None, + "dataloggersn": "", + "equipment_name": None, + "hasmeter": False, + "meter_type": None, + "pre_hour_lasttotal": 23920.7, + "pre_hour_time": 1698137988291, + "current_hour_pv": 0, + "extend_properties": None, + "eP_connect_status_happen": None, + "eP_connect_status_recover": None, + "total_sell": 0, + "total_buy": 0, + "errors": [], + "safetyConutry": 9, + "deratingMode": None, + "master": None, + "parallel_code": None, + "inverter_type": 0, + "total_pbattery": 0, + "battery_count": None, + "more_batterys": None, + "genset_start_mode": None, + "genset_mode": None, + "genset_etotal": None, + "genset_yeasterdayTotal": None, + "genset_power": None, + "genset_eday": 0, + "all_eday": 32.2, + "output_etotal": 23920.7, + "output_yeasterdayTotal": 23888.5, + "output_eday": 32.2, + "battery_index": None, + }, + "time": "10/24/2023 22:33:53", + "battery": "0V/0A/0W", + "firmware_version": 191917, + "warning_bms": "--", + "soh": "0%", + "discharge_current_limit_bms": "0A", + "charge_current_limit_bms": "0A", + "soc": "519%", + "pv_input_2": "0V/0A", + "pv_input_1": "0V/0A", + "back_up_output": "0V/0W", + "output_voltage": "0V", + "backup_voltage": "0V", + "output_current": "0A", + "output_power": "0W", + "total_generation": "23920.7kWh", + "daily_generation": "32.2kWh", + "battery_charging": "0V/0A/0W", + "last_refresh_time": "10/24/2023 20:12:49", + "bms_status": "", + "pw_id": "ed5e828b-714c-446e-910e-864aa6bd6e8f", + "fault_message": "", + "warning_code": None, + "battery_power": 0, + "point_index": "2", + "points": [ + { + "target_index": 1, + "target_name": "Vpv1", + "display": "Vpv1(V)", + "unit": "V", + "target_key": "Vpv1", + "text_cn": "直流电压1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 2, + "target_name": "Vpv2", + "display": "Vpv2(V)", + "unit": "V", + "target_key": "Vpv2", + "text_cn": "直流电压2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 5, + "target_name": "Ipv1", + "display": "Ipv1(A)", + "unit": "A", + "target_key": "Ipv1", + "text_cn": "直流电流1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 6, + "target_name": "Ipv2", + "display": "Ipv2(A)", + "unit": "A", + "target_key": "Ipv2", + "text_cn": "直流电流2", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 9, + "target_name": "Ua", + "display": "Ua(V)", + "unit": "V", + "target_key": "Vac1", + "text_cn": "交流电压1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 12, + "target_name": "Iac1", + "display": "Iac1(A)", + "unit": "A", + "target_key": "Iac1", + "text_cn": "交流电流1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 15, + "target_name": "Fac1", + "display": "Fac1(Hz)", + "unit": "Hz", + "target_key": "Fac1", + "text_cn": "频率1", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 18, + "target_name": "Power", + "display": "Power(W)", + "unit": "W", + "target_key": "Pac", + "text_cn": "功率", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 19, + "target_name": "WorkMode", + "display": "WorkMode", + "unit": "", + "target_key": "WorkMode", + "text_cn": "工作模式", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 20, + "target_name": "Temperature", + "display": "Temperature(℃)", + "unit": "℃", + "target_key": "Tempperature", + "text_cn": "温度", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 23, + "target_name": "HTotal", + "display": "HTotal(h)", + "unit": "h", + "target_key": "HTotal", + "text_cn": "工作时长", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + { + "target_index": 36, + "target_name": "RSSI", + "display": "RSSI(%)", + "unit": "%", + "target_key": "Reserved5", + "text_cn": "GPRS信号强度", + "target_sn_six": None, + "target_sn_seven": None, + "target_type": None, + "storage_name": None, + }, + ], + "backup_pload_s": 0, + "backup_vload_s": 0, + "backup_iload_s": 0, + "backup_pload_t": 0, + "backup_vload_t": 0, + "backup_iload_t": 0, + "etotal_buy": None, + "eday_buy": 0, + "ebattery_charge": None, + "echarge_day": 0, + "ebattery_discharge": None, + "edischarge_day": 0, + "batt_strings": 6553.5, + "meter_connect_status": None, + "mtactivepower_r": 0, + "mtactivepower_s": 0, + "mtactivepower_t": 0, + "has_tigo": False, + "canStartIV": False, + "battery_count": None, + } + ], + "hjgx": {"co2": 19.75600365, "tree": 1082.9143425, "coal": 8.0054418}, + "homKit": {"homeKitLimit": True, "sn": "HOME_KIT_SN"}, + "isTigo": False, + "tigoIntervalTimeMinute": 15, + "smuggleInfo": { + "isAllSmuggle": False, + "isSmuggle": False, + "descriptionText": None, + "sns": None, + }, + "hasPowerflow": True, + "hasGenset": False, + "powerflow": { + "pv": "0(W)", + "pvStatus": 0, + "bettery": "0(W)", + "betteryStatus": 0, + "betteryStatusStr": None, + "load": "749(W)", + "loadStatus": 1, + "grid": "749(W)", + "soc": 0, + "socText": "0%", + "hasEquipment": True, + "gridStatus": 1, + "isHomKit": True, + "isBpuAndInverterNoBattery": False, + "isMoreBettery": True, + "genset": "0(W)", + "gensetStatus": 0, + "gridGensetStatus": 0, + }, + "hasGridLoad": False, + "hasMicroInverter": False, + "hasLayout": False, + "layout_id": "", + "isParallelInventers": False, + "isEvCharge": False, + "evCharge": None, + "hasEnergeStatisticsCharts": True, + "energeStatisticsCharts": { + "contributingRate": 0.4, + "selfUseRate": 0.39, + "sum": 32.19, + "buy": 18.32, + "buyPercent": 59.6, + "sell": 19.78, + "sellPercent": 61.4, + "selfUseOfPv": 12.41, + "consumptionOfLoad": 30.73, + "chartsType": 1, + "hasPv": True, + "hasCharge": False, + "charge": 0, + "disCharge": 0, + "gensetGen": 0, + "hasGenset": False, + }, + "energeStatisticsTotals": { + "contributingRate": 0, + "selfUseRate": 0, + "sum": 19754.41, + "buy": 19719.9, + "buyPercent": 0, + "sell": 11248.1, + "sellPercent": 0, + "selfUseOfPv": 0, + "consumptionOfLoad": 0, + "chartsType": 1, + "hasPv": True, + "hasCharge": False, + "charge": 0, + "disCharge": 0, + "gensetGen": 0, + "hasGenset": False, + }, + "soc": {"power": 0, "status": 0}, + "environmental": [], + "equipment": [ + { + "type": "5", + "title": "House", + "status": -1, + "model": None, + "statusText": None, + "capacity": None, + "actionThreshold": None, + "subordinateEquipment": "", + "powerGeneration": "Power:0kW", + "eday": "Generation Today: 32.2kWh", + "brand": "", + "isStored": False, + "soc": "SOC:519%", + "isChange": False, + "relationId": "edd93607-a44d-4647-86e5-afc5168d76a4", + "sn": "FAKE_SN", + "has_tigo": False, + "is_sec": False, + "is_secs": False, + "targetPF": None, + "exportPowerlimit": None, + "titleSn": None, + }, + { + "type": "1", + "title": "homekit", + "status": 1, + "model": None, + "statusText": None, + "capacity": None, + "actionThreshold": None, + "subordinateEquipment": "Data Logger: HOME_KIT_SN", + "powerGeneration": None, + "eday": None, + "brand": None, + "isStored": False, + "soc": None, + "isChange": False, + "relationId": "cc6bd791-45e7-4964-bed0-53f138b97310", + "sn": "HOME_KIT_SN", + "has_tigo": False, + "is_sec": False, + "is_secs": False, + "targetPF": None, + "exportPowerlimit": None, + "titleSn": None, + }, + ], +} diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 0000000..f0265d7 --- /dev/null +++ b/tests/test.py @@ -0,0 +1,62 @@ +from custom_components.sems.sensor import sensor_options_for_data +from data.test_data_001 import data_001 +from data.test_data_002 import data_002 + + +def get_value_from_path(data, path): + """ + Get value from a nested dictionary. + """ + value = data + try: + for key in path: + value = value[key] + except KeyError: + return None + return value + + +def len_of_all_list_items(list_of_str): + return sum([len(str(item)) for item in list_of_str]) + + +def print_sensor_options(data, sensor_options): + max_length = max( + [ + len_of_all_list_items(sensor_option.value_path) + + (len(sensor_option.value_path) * 2) + for sensor_option in sensor_options + ] + ) + for sensor_option in sensor_options: + value = get_value_from_path(data, sensor_option.value_path) + path = "" + for key in sensor_option.value_path: + path += str(key) + "->" + path = path[:-2] + path += ":" + path += " " * ( + max_length + - ( + len_of_all_list_items(sensor_option.value_path) + + (len(sensor_option.value_path) * 2) + ) + ) + print(f"{path} {value}") + + +def check_value_paths(data, sensor_options): + for sensor_option in sensor_options: + value = get_value_from_path(data, sensor_option.value_path) + if value is None: + print("Value not found for path: {}".format(sensor_option.value_path)) + + +if __name__ == "__main__": + for data_info in [("data_001", data_001), ("data_002", data_002)]: + (name, data) = data_info + print("\n") + print(f"===== {name} =====") + sensor_options = sensor_options_for_data(data) + check_value_paths(data, sensor_options) + print_sensor_options(data, sensor_options) From 4bb29f409d18c9478d5d84fe2ff7d39453aa07f4 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Thu, 9 Nov 2023 23:50:12 +1100 Subject: [PATCH 11/17] Add more_batterys --- custom_components/sems/sensor.py | 78 ++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 60cf840..8af4dd1 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -335,6 +335,84 @@ def sensor_options_for_data(data) -> List[SensorOptions]: SensorStateClass.MEASUREMENT, ), ] + battery_count = get_value_from_path(data, path_to_inverter + ["battery_count"]) + if battery_count is not None: + for idx in range(0, battery_count): + path_to_battery = path_to_inverter + ["more_batterys", idx] + sensors += [ + SensorOptions( + device_info, + f"{serial_number}-{idx}-pbattery", + f"Inverter {inverter['name']} Battery {idx} Power", + path_to_battery + ["pbattery"], + SensorDeviceClass.POWER, + POWER_WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-{idx}-vbattery", + f"Inverter {inverter['name']} Battery {idx} Voltage", + path_to_battery + ["vbattery"], + SensorDeviceClass.VOLTAGE, + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-{idx}-ibattery", + f"Inverter {inverter['name']} Battery {idx} Current", + path_to_battery + ["ibattery"], + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-{idx}-soc", + f"Inverter {inverter['name']} Battery {idx} State of Charge", + path_to_battery + ["soc"], + SensorDeviceClass.BATTERY, + PERCENTAGE, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-{idx}-soh", + f"Inverter {inverter['name']} Battery {idx} State of Health", + path_to_battery + ["soh"], + SensorDeviceClass.BATTERY, + PERCENTAGE, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-{idx}-bms_temperature", + f"Inverter {inverter['name']} Battery {idx} BMS Temperature", + path_to_battery + ["bms_temperature"], + SensorDeviceClass.TEMPERATURE, + TEMP_CELSIUS, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-{idx}-bms_discharge_i_max", + f"Inverter {inverter['name']} Battery {idx} BMS Discharge Max Current", + path_to_battery + ["bms_discharge_i_max"], + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-{idx}-bms_charge_i_max", + f"Inverter {inverter['name']} Battery {idx} BMS Charge Max Current", + path_to_battery + ["bms_charge_i_max"], + SensorDeviceClass.CURRENT, + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + ), + ] if "hasPowerflow" in data and data["hasPowerflow"] and "powerflow" in data: serial_number = "powerflow" From 332e2e2048f10fee1e2aed6140b04a6c4adbb803 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Fri, 17 Nov 2023 19:49:47 +1100 Subject: [PATCH 12/17] Check for existing homekit, for backwards compatibility. --- custom_components/sems/sensor.py | 35 +++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 8af4dd1..0361327 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -31,6 +31,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry as entities_for_config_entry, + async_get as async_get_entity_registry, +) from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -138,7 +142,24 @@ def __str__(self): return f"SensorOptions(device_info={self.device_info}, unique_id={self.unique_id}, name={self.name}, value_path={self.value_path}, device_class={self.device_class}, native_unit_of_measurement={self.native_unit_of_measurement}, state_class={self.state_class}, empty_value={self.empty_value}, data_type_converter={self.data_type_converter})" -def sensor_options_for_data(data) -> List[SensorOptions]: +def get_home_kit_sn(data): + return get_value_from_path(data, [GOODWE_SPELLING.homeKit, "sn"]) + + +def get_has_existing_homekit_entity(data, hass, config_entry) -> bool: + home_kit_sn = get_home_kit_sn(data) + if home_kit_sn is not None: + ent_reg = async_get_entity_registry(hass) + entities = entities_for_config_entry(ent_reg, config_entry.entry_id) + for entity in entities: + if entity.unique_id == home_kit_sn: + return True + return False + + +def sensor_options_for_data( + data, has_existing_homekit_entity=False +) -> List[SensorOptions]: sensors: List[SensorOptions] = [] try: currency = data["kpi"]["currency"] @@ -415,7 +436,9 @@ def sensor_options_for_data(data) -> List[SensorOptions]: ] if "hasPowerflow" in data and data["hasPowerflow"] and "powerflow" in data: - serial_number = "powerflow" + inverter_serial_number = get_home_kit_sn(data) + if not has_existing_homekit_entity or inverter_serial_number is None: + inverter_serial_number = "powerflow" serial_backwards_compatibility = ( "homeKit" # the old code uses homeKit for the serial number ) @@ -592,7 +615,13 @@ async def async_update_data(): data = coordinator.data - sensor_options: List[SensorOptions] = sensor_options_for_data(data) + has_existing_homekit_entity = get_has_existing_homekit_entity(data, hass, config_entry) + + _LOGGER.warning("has_existing_homekit_entity: %s", has_existing_homekit_entity) + + sensor_options: List[SensorOptions] = sensor_options_for_data( + data, has_existing_homekit_entity + ) sensors: List[Sensor] = [] for sensor_option in sensor_options: sensors.append( From 1471caf9fae05685097243298557edf405726b67 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Sat, 27 Jan 2024 19:22:14 +1100 Subject: [PATCH 13/17] Fix bug with detecting existing homekit_entity Try and handle bug in the goodwe api where it returns zero when close to midnight. @see https://github.com/TimSoethout/goodwe-sems-home-assistant/issues/94 --- custom_components/sems/sensor.py | 47 ++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 0361327..3e66e63 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -7,7 +7,7 @@ import logging import re -from datetime import timedelta +from datetime import timedelta, datetime from decimal import Decimal from typing import List, Callable, Optional @@ -88,7 +88,7 @@ def __init__( self._value_path = value_path self._data_type_converter = data_type_converter self._empty_value = empty_value - + self._attr_available = self._get_native_value_from_coordinator() is not None self._attr_unique_id = unique_id self._attr_name = name self._attr_native_unit_of_measurement = native_unit_of_measurement @@ -102,11 +102,13 @@ def _get_native_value_from_coordinator(self): @property def native_value(self): """Return the state of the device.""" - value = get_value_from_path(self.coordinator.data, self._value_path) + value = self._get_native_value_from_coordinator() if isinstance(value, str): value = self.str_clean_regex.search(value).group(1) if self._empty_value is not None and self._empty_value == value: value = 0 + if value is None: + return value typed_value = self._data_type_converter(value) return typed_value @@ -157,9 +159,9 @@ def get_has_existing_homekit_entity(data, hass, config_entry) -> bool: return False -def sensor_options_for_data( - data, has_existing_homekit_entity=False -) -> List[SensorOptions]: +def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> List[SensorOptions]: + if has_existing_homekit_entity is None: + has_existing_homekit_entity = False sensors: List[SensorOptions] = [] try: currency = data["kpi"]["currency"] @@ -453,7 +455,7 @@ def sensor_options_for_data( sensors += [ SensorOptions( device_info, - f"{serial_number}", # backwards compatibility otherwise would be f"{serial_number}-load" + f"{inverter_serial_number}", # backwards compatibility otherwise would be f"{serial_number}-load" f"HomeKit Load", ["powerflow", "load"], SensorDeviceClass.POWER, @@ -462,7 +464,7 @@ def sensor_options_for_data( ), SensorOptions( device_info, - f"{serial_number}-pv", + f"{inverter_serial_number}-pv", f"HomeKit PV", ["powerflow", "pv"], SensorDeviceClass.POWER, @@ -471,7 +473,7 @@ def sensor_options_for_data( ), SensorOptions( device_info, - f"{serial_number}-grid", + f"{inverter_serial_number}-grid", f"HomeKit Grid", ["powerflow", "grid"], SensorDeviceClass.POWER, @@ -480,7 +482,7 @@ def sensor_options_for_data( ), SensorOptions( device_info, - f"{serial_number}-battery", + f"{inverter_serial_number}-battery", f"HomeKit Battery", ["powerflow", GOODWE_SPELLING.battery], SensorDeviceClass.POWER, @@ -489,7 +491,7 @@ def sensor_options_for_data( ), SensorOptions( device_info, - f"{serial_number}-genset", + f"{inverter_serial_number}-genset", f"HomeKit generator", ["powerflow", "genset"], SensorDeviceClass.POWER, @@ -498,7 +500,7 @@ def sensor_options_for_data( ), SensorOptions( device_info, - f"{serial_number}-soc", + f"{inverter_serial_number}-soc", f"HomeKit State of Charge", ["powerflow", "soc"], SensorDeviceClass.BATTERY, @@ -514,7 +516,7 @@ def sensor_options_for_data( sensors += [ SensorOptions( device_info, - f"{serial_number}-import-energy", + f"{inverter_serial_number}-import-energy", f"Sems Import", [GOODWE_SPELLING.energyStatisticsCharts, "buy"], SensorDeviceClass.ENERGY, @@ -523,7 +525,7 @@ def sensor_options_for_data( ), SensorOptions( device_info, - f"{serial_number}-export-energy", + f"{inverter_serial_number}-export-energy", f"Sems Export", [GOODWE_SPELLING.energyStatisticsCharts, "sell"], SensorDeviceClass.ENERGY, @@ -535,7 +537,7 @@ def sensor_options_for_data( sensors += [ SensorOptions( device_info, - f"{serial_number}-import-energy-total", + f"{inverter_serial_number}-import-energy-total", f"Sems Total Import", [GOODWE_SPELLING.energyStatisticsTotals, "buy"], SensorDeviceClass.ENERGY, @@ -544,7 +546,7 @@ def sensor_options_for_data( ), SensorOptions( device_info, - f"{serial_number}-export-energy-total", + f"{inverter_serial_number}-export-energy-total", f"Sems Total Export", [GOODWE_SPELLING.energyStatisticsTotals, "sell"], SensorDeviceClass.ENERGY, @@ -586,6 +588,15 @@ async def async_update_data(): inverters = result["inverter"] if inverters is None: raise UpdateFailed(API_UPDATE_ERROR_MSG) + # try and handle bug in the goodwe api where it returns zero when close to midnight. + # @see https://github.com/TimSoethout/goodwe-sems-home-assistant/issues/94 + if GOODWE_SPELLING.energyStatisticsCharts in result: + now = datetime.now() + past_11_45 = now.hour > 11 and now.minute > 45 + before_01_15 = now.hour < 1 and now.minute < 15 + if past_11_45 or before_01_15: + result[GOODWE_SPELLING.energyStatisticsCharts]["sell"] = None + return result # except ApiError as err: except Exception as err: @@ -615,7 +626,9 @@ async def async_update_data(): data = coordinator.data - has_existing_homekit_entity = get_has_existing_homekit_entity(data, hass, config_entry) + has_existing_homekit_entity = get_has_existing_homekit_entity( + data, hass, config_entry + ) _LOGGER.warning("has_existing_homekit_entity: %s", has_existing_homekit_entity) From a624c1bca0d330fbc3e2af239ee11dd465430bd0 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Wed, 7 Feb 2024 21:03:45 +1100 Subject: [PATCH 14/17] datetime.now() is 24hour time. "buy" chart is also bugged around midnight. --- custom_components/sems/const.py | 6 +++--- custom_components/sems/sems_api.py | 2 +- custom_components/sems/sensor.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/sems/const.py b/custom_components/sems/const.py index 9da008d..05d512e 100644 --- a/custom_components/sems/const.py +++ b/custom_components/sems/const.py @@ -1,10 +1,9 @@ """Constants for the sems integration.""" - -DOMAIN = "sems" - import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL +DOMAIN = "sems" + CONF_STATION_ID = "powerstation_id" DEFAULT_SCAN_INTERVAL = 60 # timedelta(seconds=60) @@ -27,6 +26,7 @@ AC_CURRENT_EMPTY = 6553.5 AC_FEQ_EMPTY = 655.35 + class GOODWE_SPELLING: battery = "bettery" homeKit = "homKit" diff --git a/custom_components/sems/sems_api.py b/custom_components/sems/sems_api.py index 98f92e1..42a697e 100644 --- a/custom_components/sems/sems_api.py +++ b/custom_components/sems/sems_api.py @@ -32,7 +32,7 @@ def test_authentication(self) -> bool: self._token = self.getLoginToken(self._username, self._password) return self._token is not None except Exception as exception: - _LOGGER.exception("SEMS Authentication exception " + exception) + _LOGGER.exception("SEMS Authentication exception " + exception.__str__()) return False def getLoginToken(self, userName, password): diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 3e66e63..5a31197 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -592,11 +592,11 @@ async def async_update_data(): # @see https://github.com/TimSoethout/goodwe-sems-home-assistant/issues/94 if GOODWE_SPELLING.energyStatisticsCharts in result: now = datetime.now() - past_11_45 = now.hour > 11 and now.minute > 45 - before_01_15 = now.hour < 1 and now.minute < 15 + past_11_45 = now.hour == 23 and now.minute > 45 + before_01_15 = now.hour == 0 and now.minute < 15 if past_11_45 or before_01_15: result[GOODWE_SPELLING.energyStatisticsCharts]["sell"] = None - + result[GOODWE_SPELLING.energyStatisticsCharts]["buy"] = None return result # except ApiError as err: except Exception as err: From 766e0ba508a7c8482ad691bf68bf19635ba13b1b Mon Sep 17 00:00:00 2001 From: David Christen Date: Wed, 20 Mar 2024 10:32:07 +0100 Subject: [PATCH 15/17] Use new constants for units Signed-off-by: David Christen --- custom_components/sems/sensor.py | 75 ++++++++++++++++---------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 5a31197..1d8180f 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -18,15 +18,16 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - POWER_WATT, + UnitOfEnergy, + UnitOfPower, CONF_SCAN_INTERVAL, - ENERGY_KILO_WATT_HOUR, - TEMP_CELSIUS, - ELECTRIC_POTENTIAL_VOLT, - ELECTRIC_CURRENT_AMPERE, - FREQUENCY_HERTZ, - PERCENTAGE, - POWER_KILO_WATT, + UnitOfEnergy, + UnitOfTemperature, + UnitOfElectricPotential, + UnitOfElectricCurrent, + UnitOfTemperature, + UnitOfFrequency, + PERCENTAGE ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -190,7 +191,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Capacity", path_to_inverter + ["capacity"], SensorDeviceClass.POWER, - POWER_KILO_WATT, + UnitOfPower.KILO_WATT, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -199,7 +200,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Power", path_to_inverter + ["pac"], SensorDeviceClass.POWER, - POWER_WATT, + UnitOfPower.WATT, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -208,7 +209,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Energy", path_to_inverter + ["etotal"], SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, ), SensorOptions( @@ -224,7 +225,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Temperature", path_to_inverter + [GOODWE_SPELLING.temperature], SensorDeviceClass.TEMPERATURE, - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -233,7 +234,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Energy Today", path_to_inverter + ["eday"], SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, ), SensorOptions( @@ -242,7 +243,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Energy This Month", path_to_inverter + [GOODWE_SPELLING.thisMonthTotalE], SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, ), SensorOptions( @@ -251,7 +252,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Energy Last Month", path_to_inverter + [GOODWE_SPELLING.lastMonthTotalE], SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, ), SensorOptions( @@ -280,7 +281,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} PV String {idx} Voltage", path_to_inverter + [f"vpv{idx}"], SensorDeviceClass.VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, + UnitOfElectricPotential.VOLT, SensorStateClass.MEASUREMENT, ) for idx in range(1, 5) @@ -293,7 +294,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} PV String {idx} Current", path_to_inverter + [f"ipv{idx}"], SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, + UnitOfElectricCurrent.AMPERE, SensorStateClass.MEASUREMENT, ) for idx in range(1, 5) @@ -306,7 +307,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Grid {idx} AC Voltage", path_to_inverter + [f"vac{idx}"], SensorDeviceClass.VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, + UnitOfElectricPotential.VOLT, SensorStateClass.MEASUREMENT, AC_EMPTY, ) @@ -319,7 +320,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Grid {idx} AC Current", path_to_inverter + [f"iac{idx}"], SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, + UnitOfElectricCurrent.AMPERE, SensorStateClass.MEASUREMENT, AC_CURRENT_EMPTY, ) @@ -332,7 +333,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Grid {idx} AC Frequency", path_to_inverter + [f"fac{idx}"], SensorDeviceClass.FREQUENCY, - FREQUENCY_HERTZ, + UnitOfFrequency.HERTZ, SensorStateClass.MEASUREMENT, AC_FEQ_EMPTY, ) @@ -345,7 +346,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Battery Voltage", path_to_inverter + ["vbattery1"], SensorDeviceClass.VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, + UnitOfElectricPotential.VOLT, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -354,7 +355,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Battery Current", path_to_inverter + ["ibattery1"], SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, + UnitOfElectricCurrent.AMPERE, SensorStateClass.MEASUREMENT, ), ] @@ -369,7 +370,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Battery {idx} Power", path_to_battery + ["pbattery"], SensorDeviceClass.POWER, - POWER_WATT, + UnitOfPower.WATT, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -378,7 +379,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Battery {idx} Voltage", path_to_battery + ["vbattery"], SensorDeviceClass.VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, + UnitOfElectricPotential.VOLT, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -387,7 +388,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Battery {idx} Current", path_to_battery + ["ibattery"], SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, + UnitOfElectricCurrent.AMPERE, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -414,7 +415,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Battery {idx} BMS Temperature", path_to_battery + ["bms_temperature"], SensorDeviceClass.TEMPERATURE, - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -423,7 +424,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Battery {idx} BMS Discharge Max Current", path_to_battery + ["bms_discharge_i_max"], SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, + UnitOfElectricCurrent.AMPERE, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -432,7 +433,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Inverter {inverter['name']} Battery {idx} BMS Charge Max Current", path_to_battery + ["bms_charge_i_max"], SensorDeviceClass.CURRENT, - ELECTRIC_CURRENT_AMPERE, + UnitOfElectricCurrent.AMPERE, SensorStateClass.MEASUREMENT, ), ] @@ -459,7 +460,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"HomeKit Load", ["powerflow", "load"], SensorDeviceClass.POWER, - POWER_WATT, + UnitOfPower.WATT, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -468,7 +469,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"HomeKit PV", ["powerflow", "pv"], SensorDeviceClass.POWER, - POWER_WATT, + UnitOfPower.WATT, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -477,7 +478,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"HomeKit Grid", ["powerflow", "grid"], SensorDeviceClass.POWER, - POWER_WATT, + UnitOfPower.WATT, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -486,7 +487,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"HomeKit Battery", ["powerflow", GOODWE_SPELLING.battery], SensorDeviceClass.POWER, - POWER_WATT, + UnitOfPower.WATT, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -495,7 +496,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"HomeKit generator", ["powerflow", "genset"], SensorDeviceClass.POWER, - POWER_WATT, + UnitOfPower.WATT, SensorStateClass.MEASUREMENT, ), SensorOptions( @@ -520,7 +521,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Sems Import", [GOODWE_SPELLING.energyStatisticsCharts, "buy"], SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, ), SensorOptions( @@ -529,7 +530,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Sems Export", [GOODWE_SPELLING.energyStatisticsCharts, "sell"], SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, ), ] @@ -541,7 +542,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Sems Total Import", [GOODWE_SPELLING.energyStatisticsTotals, "buy"], SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, ), SensorOptions( @@ -550,7 +551,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L f"Sems Total Export", [GOODWE_SPELLING.energyStatisticsTotals, "sell"], SensorDeviceClass.ENERGY, - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, ), ] From 81a134ca1f09ad53f9e34331dc09660e565ad61c Mon Sep 17 00:00:00 2001 From: David Christen Date: Thu, 21 Mar 2024 20:33:53 +0100 Subject: [PATCH 16/17] Added additional sensor to detect direction of power between grid and home --- custom_components/sems/sensor.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 1d8180f..f651ffc 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -481,6 +481,15 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L UnitOfPower.WATT, SensorStateClass.MEASUREMENT, ), + SensorOptions( + device_info, + f"{inverter_serial_number}-load-status", + f"HomeKit Load Status", + ["powerflow", "loadStatus"], + None, + None, + SensorStateClass.MEASUREMENT, + ), SensorOptions( device_info, f"{inverter_serial_number}-battery", From 7d4e79a4396b1d3dbb06f3fd8837e199691303e3 Mon Sep 17 00:00:00 2001 From: Kyah Rindlisbacher Date: Tue, 26 Mar 2024 20:04:10 +1100 Subject: [PATCH 17/17] Add handler to multiply load, grid, battery by the relevant xStatus --- custom_components/sems/const.py | 1 + custom_components/sems/sensor.py | 35 +++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/custom_components/sems/const.py b/custom_components/sems/const.py index 05d512e..3b9855b 100644 --- a/custom_components/sems/const.py +++ b/custom_components/sems/const.py @@ -29,6 +29,7 @@ class GOODWE_SPELLING: battery = "bettery" + batteryStatus = "betteryStatus" homeKit = "homKit" temperature = "tempperature" hasEnergyStatisticsCharts = "hasEnergeStatisticsCharts" diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 5a31197..85678b9 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -83,6 +83,7 @@ def __init__( native_unit_of_measurement: Optional[str] = None, state_class: Optional[SensorStateClass] = None, empty_value=None, + custom_value_handler=None, ): super().__init__(coordinator) self._value_path = value_path @@ -95,6 +96,7 @@ def __init__( self._attr_device_class = device_class self._attr_state_class = state_class self._attr_device_info = device_info + self._custom_value_handler = custom_value_handler def _get_native_value_from_coordinator(self): return get_value_from_path(self.coordinator.data, self._value_path) @@ -110,6 +112,8 @@ def native_value(self): if value is None: return value typed_value = self._data_type_converter(value) + if self._custom_value_handler is not None: + return self._custom_value_handler(typed_value, self.coordinator.data) return typed_value @property @@ -129,6 +133,7 @@ def __init__( state_class: Optional[SensorStateClass] = None, empty_value=None, data_type_converter=Decimal, + custom_value_handler=None, ): self.device_info = device_info self.unique_id = unique_id @@ -139,6 +144,7 @@ def __init__( self.state_class = state_class self.empty_value = empty_value self.data_type_converter = data_type_converter + self.custom_value_handler = custom_value_handler def __str__(self): return f"SensorOptions(device_info={self.device_info}, unique_id={self.unique_id}, name={self.name}, value_path={self.value_path}, device_class={self.device_class}, native_unit_of_measurement={self.native_unit_of_measurement}, state_class={self.state_class}, empty_value={self.empty_value}, data_type_converter={self.data_type_converter})" @@ -159,7 +165,9 @@ def get_has_existing_homekit_entity(data, hass, config_entry) -> bool: return False -def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> List[SensorOptions]: +def sensor_options_for_data( + data, has_existing_homekit_entity: bool | None +) -> List[SensorOptions]: if has_existing_homekit_entity is None: has_existing_homekit_entity = False sensors: List[SensorOptions] = [] @@ -452,6 +460,27 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L name="HomeKit", manufacturer="GoodWe", ) + + def status_value_handler(status_path): + """ + Handler for values that are dependent on the status of the grid. + @param status_path: the path to the status value + """ + def value_status_handler(value, data): + """ + Handler for the value that is dependent on the status of the grid. + @param value: the value to be handled + @param data: the data from the coordinator + """ + if value is None: + return None + grid_status = get_value_from_path(data, status_path) + if grid_status is None: + return value + return value * int(grid_status) + + return value_status_handler + sensors += [ SensorOptions( device_info, @@ -461,6 +490,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L SensorDeviceClass.POWER, POWER_WATT, SensorStateClass.MEASUREMENT, + status_value_handler(["powerflow", "loadStatus"]), ), SensorOptions( device_info, @@ -479,6 +509,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L SensorDeviceClass.POWER, POWER_WATT, SensorStateClass.MEASUREMENT, + status_value_handler(["powerflow", "gridStatus"]), ), SensorOptions( device_info, @@ -488,6 +519,7 @@ def sensor_options_for_data(data, has_existing_homekit_entity: bool | None) -> L SensorDeviceClass.POWER, POWER_WATT, SensorStateClass.MEASUREMENT, + status_value_handler(["powerflow", GOODWE_SPELLING.batteryStatus]), ), SensorOptions( device_info, @@ -649,6 +681,7 @@ async def async_update_data(): sensor_option.native_unit_of_measurement, sensor_option.state_class, sensor_option.empty_value, + sensor_option.custom_value_handler, ) ) async_add_entities(sensors)