diff --git a/custom_components/sems/const.py b/custom_components/sems/const.py index 684952a..3b9855b 100644 --- a/custom_components/sems/const.py +++ b/custom_components/sems/const.py @@ -1,11 +1,8 @@ """Constants for the sems integration.""" - -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 + +DOMAIN = "sems" CONF_STATION_ID = "powerstation_id" @@ -22,3 +19,21 @@ ): 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" + batteryStatus = "betteryStatus" + homeKit = "homKit" + temperature = "tempperature" + hasEnergyStatisticsCharts = "hasEnergeStatisticsCharts" + energyStatisticsCharts = "energeStatisticsCharts" + energyStatisticsTotals = "energeStatisticsTotals" + thisMonthTotalE = "thismonthetotle" + lastMonthTotalE = "lastmonthetotle" diff --git a/custom_components/sems/manifest.json b/custom_components/sems/manifest.json index 6180657..9bc8eaf 100644 --- a/custom_components/sems/manifest.json +++ b/custom_components/sems/manifest.json @@ -4,9 +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", - "iot_class": "cloud_polling", + "integration_type": "hub", + "iot_class": "cloud_polling", "requirements": [], "dependencies": [], - "codeowners": ["@TimSoethout"], - "version": "3.7.3" + "codeowners": ["@TimSoethout", "@L-four"], + "version": "4.0.0" } diff --git a/custom_components/sems/sems_api.py b/custom_components/sems/sems_api.py index e181c95..42a697e 100644 --- a/custom_components/sems/sems_api.py +++ b/custom_components/sems/sems_api.py @@ -1,15 +1,12 @@ -import json import logging - +import json 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 = { @@ -35,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 7729734..2dd3544 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -5,38 +5,609 @@ https://github.com/TimSoethout/goodwe-sems-home-assistant """ -from homeassistant.core import HomeAssistant -import homeassistant import logging - -from datetime import timedelta - +import re +from datetime import timedelta, datetime +from decimal import Decimal +from typing import List, Callable, Optional + +from homeassistant.components.sensor import ( + SensorEntity, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + UnitOfEnergy, + UnitOfPower, + CONF_SCAN_INTERVAL, + UnitOfEnergy, + UnitOfTemperature, + UnitOfElectricPotential, + UnitOfElectricCurrent, + UnitOfTemperature, + UnitOfFrequency, + PERCENTAGE +) +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, UpdateFailed, ) -from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING, SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_POWER, - POWER_WATT, - CONF_SCAN_INTERVAL, - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, + +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 homeassistant.helpers.entity import Entity -from .const import DOMAIN, CONF_STATION_ID, DEFAULT_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +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 + + +class Sensor(CoordinatorEntity, SensorEntity): + str_clean_regex = re.compile(r"(\d+\.?\d*)") + + 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, + custom_value_handler=None, + ): + super().__init__(coordinator) + 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 + 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) + + @property + def native_value(self): + """Return the state of the device.""" + 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) + if self._custom_value_handler is not None: + return self._custom_value_handler(typed_value, self.coordinator.data) + return typed_value + + @property + 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, + custom_value_handler=None, + ): + 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 + 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})" + + +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: bool | None +) -> List[SensorOptions]: + if has_existing_homekit_entity is None: + has_existing_homekit_entity = False + 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, + UnitOfPower.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, + UnitOfPower.WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-energy", + f"Inverter {inverter['name']} Energy", + path_to_inverter + ["etotal"], + SensorDeviceClass.ENERGY, + UnitOfEnergy.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, + UnitOfTemperature.CELSIUS, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-eday ", + f"Inverter {inverter['name']} Energy Today", + path_to_inverter + ["eday"], + SensorDeviceClass.ENERGY, + UnitOfEnergy.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, + UnitOfEnergy.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, + UnitOfEnergy.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, + UnitOfElectricPotential.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, + UnitOfElectricCurrent.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, + UnitOfElectricPotential.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, + UnitOfElectricCurrent.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, + UnitOfFrequency.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, + UnitOfElectricPotential.VOLT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-ibattery1", + f"Inverter {inverter['name']} Battery Current", + path_to_inverter + ["ibattery1"], + SensorDeviceClass.CURRENT, + UnitOfElectricCurrent.AMPERE, + 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, + UnitOfPower.WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-{idx}-vbattery", + f"Inverter {inverter['name']} Battery {idx} Voltage", + path_to_battery + ["vbattery"], + SensorDeviceClass.VOLTAGE, + UnitOfElectricPotential.VOLT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{serial_number}-{idx}-ibattery", + f"Inverter {inverter['name']} Battery {idx} Current", + path_to_battery + ["ibattery"], + SensorDeviceClass.CURRENT, + UnitOfElectricCurrent.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, + UnitOfTemperature.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, + UnitOfElectricCurrent.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, + UnitOfElectricCurrent.AMPERE, + SensorStateClass.MEASUREMENT, + ), + ] + + if "hasPowerflow" in data and data["hasPowerflow"] and "powerflow" in data: + 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 + ) + device_info = DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, serial_backwards_compatibility) + }, + 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, + f"{inverter_serial_number}", # backwards compatibility otherwise would be f"{serial_number}-load" + f"HomeKit Load", + ["powerflow", "load"], + SensorDeviceClass.POWER, + UnitOfPower.WATT, + SensorStateClass.MEASUREMENT, + status_value_handler(["powerflow", "loadStatus"]), + ), + SensorOptions( + device_info, + f"{inverter_serial_number}-pv", + f"HomeKit PV", + ["powerflow", "pv"], + SensorDeviceClass.POWER, + UnitOfPower.WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{inverter_serial_number}-grid", + f"HomeKit Grid", + ["powerflow", "grid"], + SensorDeviceClass.POWER, + UnitOfPower.WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{inverter_serial_number}-load-status", + f"HomeKit Load Status", + ["powerflow", "loadStatus"], + None, + None, + SensorStateClass.MEASUREMENT, + status_value_handler(["powerflow", "gridStatus"]), + ), + SensorOptions( + device_info, + f"{inverter_serial_number}-battery", + f"HomeKit Battery", + ["powerflow", GOODWE_SPELLING.battery], + SensorDeviceClass.POWER, + UnitOfPower.WATT, + SensorStateClass.MEASUREMENT, + status_value_handler(["powerflow", GOODWE_SPELLING.batteryStatus]), + ), + SensorOptions( + device_info, + f"{inverter_serial_number}-genset", + f"HomeKit generator", + ["powerflow", "genset"], + SensorDeviceClass.POWER, + UnitOfPower.WATT, + SensorStateClass.MEASUREMENT, + ), + SensorOptions( + device_info, + f"{inverter_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"{inverter_serial_number}-import-energy", + f"Sems Import", + [GOODWE_SPELLING.energyStatisticsCharts, "buy"], + SensorDeviceClass.ENERGY, + UnitOfEnergy.KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + SensorOptions( + device_info, + f"{inverter_serial_number}-export-energy", + f"Sems Export", + [GOODWE_SPELLING.energyStatisticsCharts, "sell"], + SensorDeviceClass.ENERGY, + UnitOfEnergy.KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + ] + if data[GOODWE_SPELLING.energyStatisticsTotals]: + sensors += [ + SensorOptions( + device_info, + f"{inverter_serial_number}-import-energy-total", + f"Sems Total Import", + [GOODWE_SPELLING.energyStatisticsTotals, "buy"], + SensorDeviceClass.ENERGY, + UnitOfEnergy.KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + SensorOptions( + device_info, + f"{inverter_serial_number}-export-energy-total", + f"Sems Total Export", + [GOODWE_SPELLING.energyStatisticsTotals, "sell"], + SensorDeviceClass.ENERGY, + UnitOfEnergy.KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ), + ] + return sensors + + +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) ) @@ -54,43 +625,21 @@ 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) + # 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 == 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: # logging.exception("Something awful happened!") @@ -117,453 +666,32 @@ 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) - ) - async_add_entities( - SemsStatisticsSensor(coordinator, ent) - for idx, ent in enumerate(coordinator.data) - ) - async_add_entities( - SemsPowerflowSensor(coordinator, ent) - 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 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): - """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["pac"] if data["status"] == 1 else 0 - - 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.""" - 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 + data = coordinator.data - @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 unit_of_measurement(self): - return ENERGY_KILO_WATT_HOUR - - @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 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: - """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": { - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.sn) - }, - # "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) - ) - - 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 unit_of_measurement(self): - return ENERGY_KILO_WATT_HOUR - - @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" - - @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" - - - @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 unit_of_measurement(self): - return ENERGY_KILO_WATT_HOUR - - @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" - - @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" - - @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): - """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator) - self.coordinator = coordinator - self.sn = sn - - @property - def device_class(self): - return DEVICE_CLASS_POWER - - @property - def 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']}" - - @property - def unique_id(self) -> str: - return self.coordinator.data[self.sn]["sn"] - - @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)', '') - - return load if data["gridStatus"] == 1 else 0 - - 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 - - @property - def should_poll(self) -> bool: - """No need to poll. Coordinator notifies entity of updates.""" - return False + has_existing_homekit_entity = get_has_existing_homekit_entity( + data, hass, config_entry + ) - @property - def available(self): - """Return if entity is available.""" - return self.coordinator.last_update_success + _LOGGER.warning("has_existing_homekit_entity: %s", has_existing_homekit_entity) - @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) + sensor_options: List[SensorOptions] = sensor_options_for_data( + data, has_existing_homekit_entity + ) + 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, + sensor_option.custom_value_handler, + ) ) - - 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(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)