Repository: tijsverkoyen/HomeAssistant-FusionSolar Branch: master Commit: f1aa8953f691 Files: 34 Total size: 171.2 KB Directory structure: gitextract_784agd5_/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ ├── hacs.yaml │ └── hassfest.yaml ├── LICENSE ├── README.md ├── custom_components/ │ └── fusion_solar/ │ ├── __init__.py │ ├── config_flow.py │ ├── const.py │ ├── device_real_kpi_coordinator.py │ ├── fusion_solar/ │ │ ├── const.py │ │ ├── device_attribute_entity.py │ │ ├── energy_sensor.py │ │ ├── kiosk/ │ │ │ ├── kiosk.py │ │ │ └── kiosk_api.py │ │ ├── lifetime_plant_data_entity.py │ │ ├── openapi/ │ │ │ ├── device.py │ │ │ ├── openapi_api.py │ │ │ └── station.py │ │ ├── power_entity.py │ │ ├── realtime_device_data_sensor.py │ │ ├── station_attribute_entity.py │ │ └── year_plant_data_entity.py │ ├── manifest.json │ ├── sensor.py │ ├── strings.json │ └── translations/ │ ├── de.json │ ├── en.json │ ├── es.json │ ├── fr.json │ ├── it.json │ └── pt.json ├── docs/ │ └── postman_collection.json └── hacs.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: tijsverkoyen ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. ** Debug information ** Answer the questions below before submitting a new issue. * Are you using the latest version of Home Assistant? * Are you using the latest version of this integration? * Have you looked into the closed issues and did not find a similar issue. * Are you using Kiosk or Northbound / OpenAPI mode? * If you are using Kiosk mode. Have you checked that the url is still valid? See [I am using the kiosk mode, but the data is not updating](https://github.com/tijsverkoyen/HomeAssistant-FusionSolar/blob/master/README.md#i-am-using-the-kiosk-mode-but-the-data-is-not-updating) * If you ar using Northbound / OpenAPI mode. Check if your credentials are still valid. See [How can I verify my Northbound API / OpenAPI credentials?](https://github.com/tijsverkoyen/HomeAssistant-FusionSolar/blob/master/README.md#how-can-i-verify-my-northbound-api--openapi-credentials) * Have you checked the logs for any info? **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. ================================================ FILE: .github/workflows/hacs.yaml ================================================ name: HACS Action on: push: pull_request: schedule: - cron: "0 0 * * *" jobs: hacs: name: HACS Action runs-on: "ubuntu-latest" steps: - name: HACS Action uses: "hacs/action@main" with: category: "integration" ================================================ FILE: .github/workflows/hassfest.yaml ================================================ name: Validate with hassfest on: push: pull_request: schedule: - cron: "0 0 * * *" jobs: validate: runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v2" - uses: home-assistant/actions/hassfest@master ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Tijs Verkoyen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Home Assistant FusionSolar Integration [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration) Integrate FusionSolar into your Home Assistant. - [Home Assistant FusionSolar Integration](#home-assistant-fusionsolar-integration) - [Installation](#installation) - [Kiosk](#kiosk) - [Northbound API / OpenAPI](#northbound-api--openapi) - [Exposed Devices](#exposed-devices) - [Realtime data](#realtime-data) - [Integration with the Energy dashboard](#integration-with-the-energy-dashboard) - [FAQ](#faq) The integration is able to work with Kiosk mode, or with a Northbound API / OpenAPI account, see below for more details. ## Installation This integration is part of the default HACS repositories, so can add it directly from HACS or add this repository as a custom repository in HACS. When the integration is installed in HACS, you need to add it in Home Assistant: Settings → Devices & Services → Add Integration → Search for FusionSolar. The configuration happens in the configuration flow when you add the integration. ## Kiosk FusionSolar has a kiosk mode. The kiosk is a dashboard that is accessible for everyone that has the url. The integration uses a JSON REST api that is also consumed by the kiosk. The integration updates the data every 10 minutes. **In kiosk mode the "realtime" data is not really realtime, it is cached at FusionSolars end for 30 minutes.** The kiosk only exposes the following data: * Realtime Power * Total Current Day Energy * Total Current Month Energy * Total Current Year Energy * Total Lifetime Energy If you need more accurate information you should use the API mode. ## Northbound API / OpenAPI You will need a Northbound API / OpenAPI account from your Huawei installer for this to work. ### If you know your installer, or you have the installer account Please pass them the following guide: 1. Go to System -> Company managemet -> Northbound Management 2. Create (new 3. Set a username, password and associated account. Set the deadline 50 years into the future (or similar). 4. Tick the companies/plants and use the ">>" button to actually have it chosen 5. Enable "Basic APIs" 6. OK #### Device Data For each of the Device Data permissions, there is a choice of the following device types. Ensure your installer gives you access to each device type, and all data under each device type, based on your installation: * String Inverter * Residential Inverter * Battery * ESS * Power Sensor * Grid Meter * EMI ### If you know your current installer, but would like to manage the devices on your own There is a plant transfer process, which keeps all data. This can be found under ***Plants → Plant Migration*** in the installer interface. You will need your own installer account, and you will need to supply the losing installer with your company name and code. This can be found here: ***System → System → Company Management → Company Info*** ### If you do not know your installer There is a process to create your own installer account, but there are caveats: * It will lose all history in FusionSolar for your plant. * If you are not comfortable resetting devices and/or possibly losing access entirely, please stick with the Kiosk option or engage a new installer to take control of your plant. * Please contact Huawei Fusion Solar directly for details. ### API testing [How to login to the API](https://support.huawei.com/enterprise/en/doc/EDOC1100261860/9e1a18d2/login-interface) An example of the API url is: ```https://intl.fusionsolar.huawei.com/thirdData/``` where ```intl``` is the prefix on your own FusionSolar login page. The Northbound API has very strict rate limits on endpoints, as well as a single login session limit. If you wish to do your own testing or development alongside running this integration, it is recommended to get your installer to create 2 identical accounts. If you try to use the same account in Postman and the integration, you will experience issues such as constant directions to log back in using Postman, returned data not being complete etc. ### Exposed Devices The integration will expose the different devices (Residential inverter, String inverter, Battery, Dongle, ...) in your plant/station. ### Realtime data The devices that support realtime information (`getDevRealKpi` api call): * String inverter * EMI * Grid meter * Residential inverter * Residential Battery * Power Sensor * C&I and Utility ESS The exposed entities can be different per device. [These are documented here](https://support.huawei.com/enterprise/en/doc/EDOC1100261860/3557ba96/real-time-device-data-interface). But the names are pretty self-explanatory. The realtime data is updated every minute per device group. As the API only allows 1 call per minute to each endpoint and the same endpoint is needed for each device group. The more different devices you have the slower the updates will be. See [Disabling devices](#disabling-devices) ### Total yields The integration updates the total yields (current day, current month, current year, lifetime) every 10 minutes. ## Integration with the Energy dashboard If you have not set up the Energy dashboard in HomeAssistant, you can select it from the sidebar and step through the wizard. If you have configured it previously and want to change the settings you can access from the sidebar by clicking on [Settings → Dashboards](https://my.home-assistant.io/redirect/lovelace_dashboards/). As the name suggests, the dashboard requires sensors with units of energy as input (i.e. kWh), not power. The first thing to configure is the electricity grid: * Grid consumption is given by `sensor.xxx_reverse_active_energy` * Return to grid is given by `sensor.xxx_active_energy_forward_active_energy` For solar panels, the sensor is `sensor.xxx_total_current_day_energy`. Finally the battery needs to be configured: * Battery energy in is given by `sensor.xxx_charging_capacity` * Battery energy out is given by `sensor.xxx_discharging_capacity` ## FAQ ### Where can I find the kiosk url? 1. Login to the [FusionSolar portal](https://eu5.fusionsolar.huawei.com/) 2. Select your plant in the overview (Home → List view → Click on your plant name) 3. You will be redirect to the "Monitoring" page 4. Click on the "Kiosk" button in the top right corner 5. Enable the "Kiosk mode" in the popup if needed 6. Copy the url from the browser If you don't see the kiosk button, you are probably logged in with an installer account. ### I am using the kiosk mode, but the data is not updating First check that the Kiosk url is still working. The url is valid for 1 year. So you will need to update the kiosk url every year. ### Energy Dashboard: Active Power not showing in the list of available entities Active Power is the current power production in Watt (W) or kilo Watt (kW). The Energy dashboard expects a value in * *kWh**. Your plant, inverter(s), batteries, ... expose a lot of entities, you can see them all: Settings → Devices & Integrations → Click on the "x devices" on the Fusion Solar Integration. Click on the device you want to see the entities for. ### What do all entities mean? As I don't own an installation with all possible devices this integration is mostly based on the [Northbound Interface Reference](https://support.huawei.com/enterprise/en/doc/EDOC1100261860/d4ee355a/v6-interface-reference). The entity names are based on the names in the interface reference. ### Disabling devices / plants / stations If you have a lot of devices / plants / stations wherefore you don't want to use the data. You can disable them through the interface: Settings → Devices & Integrations → Click on the "x devices" on the Fusion Solar Integration. Click on the device / plant / station you want to disable. Click on the pencil icon in the upper right corner. Switch off "Enable device". This can speed up the updating of the other devices. Keep in mind that a call is made per device type. So if you have multiple devices from the same time you need to disable them all to have effect. You will also need to restart Home Assistant. ### Can I work with the API myself? Yes. There is a [Postman] Collection available. You can import it in Postman and start working with it. The collection is available under [docs/postman_collection](https://github.com/tijsverkoyen/HomeAssistant-FusionSolar/tree/master/docs/postman_collection.json). You will need to create an environment in Postman with the following variables: * `USERNAME`, your Northbound API / OpenAPI username * `SYSTEMCODE`, your Northbound API / OpenAPI password * `URL`, the url you will query ### I need to change the Kiosk URL The Kiosk URL is valid for 1 year. After that you will need to update the URL. At this point there is no way to update the URL without re-adding the integration. This can result in data los. You can work around this, **but this is not recommended and is at your own risk**: Open the file `.storage/core.config_entries` in the Home Assistant directory. There will be an entry like below" ```json { "entry_id": "8cd2320a2969e84XXX", "version": 1, "domain": "fusion_solar", "title": "Fusion Solar", "data": { "kiosks": [ { "name": "Foo", "url": "https://eu5.fusionsolar.huawei.com/pvmswebsite/nologin/assets/build/index.html#/kiosk?kk=XXX" } ], "credentials": {} }, ... } ``` Update the `url`. And restart Home Assistant. ### How can I verify my Northbound API / OpenAPI credentials? You can verify your credentials by doing a `curl` call. ``` curl --location 'https://eu5.fusionsolar.huawei.com/thirdData/login' \ --header 'Content-Type: application/json' \ --data '{ "userName": "XXX", "systemCode": "YYY" }' ``` Check if you need to alter the base hostname https://eu5.fusionsolar.huawei.com. And replace XXX with your username and YYY with your password. The response should look like: ```json { "data": null, "success": true, "failCode": 0, "params": {}, "message": null } ``` ================================================ FILE: custom_components/fusion_solar/__init__.py ================================================ """ Custom integration to integrate FusionSolar with Home Assistant. """ from homeassistant.core import HomeAssistant from homeassistant.core_config import Config from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.helpers import config_validation as cv from .const import DOMAIN CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up the FusionSolar component from yaml configuration.""" hass.data.setdefault(DOMAIN, {}) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the FusionSolar component from a ConfigEntry.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = entry.data # Forward the setup to the sensor platform. await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok ================================================ FILE: custom_components/fusion_solar/config_flow.py ================================================ from typing import Any, Dict, Optional from homeassistant import config_entries from homeassistant.const import CONF_NAME, CONF_URL, CONF_HOST, CONF_USERNAME, CONF_PASSWORD from .const import DOMAIN, CONF_KIOSKS, CONF_TYPE, CONF_TYPE_KIOSK, CONF_TYPE_OPENAPI, CONF_OPENAPI_CREDENTIALS from .fusion_solar.openapi.openapi_api import FusionSolarOpenApi, FusionSolarOpenApiError import voluptuous as vol import logging _LOGGER = logging.getLogger(__name__) class FusionSolarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data: Optional[Dict[str, Any]] = { CONF_KIOSKS: [], CONF_OPENAPI_CREDENTIALS: {} } async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None): """Invoked when a user initiates a flow via the user interface.""" return await self.async_step_choose_type(user_input) async def async_step_choose_type(self, user_input: Optional[Dict[str, Any]] = None): errors: Dict[str, str] = {} if user_input is not None: if user_input[CONF_TYPE] == CONF_TYPE_KIOSK: return await self.async_step_kiosk() elif user_input[CONF_TYPE] == CONF_TYPE_OPENAPI: return await self.async_step_openapi() else: errors['base'] = 'invalid_type' type_listing = { CONF_TYPE_KIOSK: 'Kiosk', CONF_TYPE_OPENAPI: 'OpenAPI', } return self.async_show_form( step_id="choose_type", data_schema=vol.Schema({ vol.Required(CONF_TYPE, default=CONF_TYPE_KIOSK): vol.In(type_listing) }), description_placeholders={ "openapi_help_url": "https://forum.huawei.com/enterprise/en/communicate-with-fusionsolar-through-an-openapi-account/thread/591478-100027", }, errors=errors, ) async def async_step_kiosk(self, user_input: Optional[Dict[str, Any]] = None): errors: Dict[str, str] = {} if user_input is not None: self.data[CONF_KIOSKS].append({ CONF_NAME: user_input[CONF_NAME], CONF_URL: user_input[CONF_URL], }) if user_input.get("add_another", False): return await self.async_step_kiosk() return self.async_create_entry( title="Fusion Solar", data=self.data, ) return self.async_show_form( step_id="kiosk", data_schema=vol.Schema({ vol.Required(CONF_NAME): str, vol.Required(CONF_URL): str, vol.Optional("add_another"): bool, }), description_placeholders={ "kiosk_portal_url": "https://eu5.fusionsolar.huawei.com/", }, errors=errors, ) async def async_step_openapi(self, user_input: Optional[Dict[str, Any]] = None): errors: Dict[str, str] = {} if user_input is not None: try: api = FusionSolarOpenApi( user_input[CONF_HOST], user_input[CONF_USERNAME], user_input[CONF_PASSWORD] ) response = await self.hass.async_add_executor_job(api.login) self.data[CONF_OPENAPI_CREDENTIALS] = { CONF_HOST: user_input[CONF_HOST], CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], } return self.async_create_entry( title="Fusion Solar", data=self.data, ) except FusionSolarOpenApiError as error: _LOGGER.debug(error) errors["base"] = "invalid_credentials" return self.async_show_form( step_id="openapi", data_schema=vol.Schema({ vol.Required(CONF_HOST, default='https://eu5.fusionsolar.huawei.com'): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, }), errors=errors, ) ================================================ FILE: custom_components/fusion_solar/const.py ================================================ """Constants for FusionSolar.""" # Base constants DOMAIN = 'fusion_solar' # Configuration CONF_KIOSKS = 'kiosks' CONF_OPENAPI_CREDENTIALS = 'credentials' CONF_TYPE = 'type' CONF_TYPE_KIOSK = 'kiosk' CONF_TYPE_OPENAPI = 'openapi' # Possible ID suffixes ID_REALTIME_POWER = 'realtime_power' ID_TOTAL_CURRENT_DAY_ENERGY = 'total_current_day_energy' ID_TOTAL_CURRENT_MONTH_ENERGY = 'total_current_month_energy' ID_TOTAL_CURRENT_YEAR_ENERGY = 'total_current_year_energy' ID_TOTAL_LIFETIME_ENERGY = 'total_lifetime_energy' # Possible Name suffixes NAME_REALTIME_POWER = 'Realtime Power' NAME_TOTAL_CURRENT_DAY_ENERGY = 'Total Current Day Energy' NAME_TOTAL_CURRENT_MONTH_ENERGY = 'Total Current Month Energy' NAME_TOTAL_CURRENT_YEAR_ENERGY = 'Total Current Year Energy' NAME_TOTAL_LIFETIME_ENERGY = 'Total Lifetime Energy' ================================================ FILE: custom_components/fusion_solar/device_real_kpi_coordinator.py ================================================ from datetime import timedelta import math import logging from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN from .fusion_solar.const import ATTR_DEVICE_REAL_KPI_DEV_ID, ATTR_DEVICE_REAL_KPI_DATA_ITEM_MAP, \ PARAM_DEVICE_TYPE_ID_STRING_INVERTER, PARAM_DEVICE_TYPE_ID_EMI, PARAM_DEVICE_TYPE_ID_GRID_METER, \ PARAM_DEVICE_TYPE_ID_RESIDENTIAL_INVERTER, PARAM_DEVICE_TYPE_ID_BATTERY, PARAM_DEVICE_TYPE_ID_POWER_SENSOR, \ PARAM_DEVICE_TYPE_ID_C_I_UTILITY_ESS from .fusion_solar.openapi.openapi_api import FusionSolarOpenApiError, FusionSolarOpenApiAccessFrequencyTooHighError _LOGGER = logging.getLogger(__name__) class DeviceRealKpiDataCoordinator(DataUpdateCoordinator): def __init__(self, hass, api, devices): self.name = 'FusionSolarOpenAPIDeviceRealKpiType' super().__init__( hass, _LOGGER, name=self.name, update_interval=timedelta(seconds=63), ) self.api = api self.devices = devices self.skip_counter = 0 self.skip = False self.counter = 0 async def _async_update_data(self): if self.should_skip: _LOGGER.info( f'{self.name} Skipped call due to rate limiting. Wait for {self.skip_for} seconds. ' + f'{self.skip_counter}/{self.counter_limit}' ) self.skip_counter += 1 return False data = {} device_ids_grouped_per_type_id = self.device_ids_grouped_per_type_id() index_to_fetch = self.counter % len(device_ids_grouped_per_type_id) type_id_to_fetch = list(device_ids_grouped_per_type_id.keys())[index_to_fetch] self.counter += 1 try: _LOGGER.debug(f'{self.name} Fetching data for type ID: {type_id_to_fetch}') response = await self.hass.async_add_executor_job( self.api.get_dev_real_kpi, device_ids_grouped_per_type_id[type_id_to_fetch], type_id_to_fetch ) self.skip = False self.skip_counter = 0 except FusionSolarOpenApiAccessFrequencyTooHighError as e: self.skip = True return False except FusionSolarOpenApiError as error: raise UpdateFailed(f'OpenAPI Error: {error}') # When there is no data we can't update. if response is None: _LOGGER.warning( f'getDevRealKpi returned a data object with no data. Check if you have sufficient permissions.' ) return False for response_data in response: key = f'{DOMAIN}-{response_data[ATTR_DEVICE_REAL_KPI_DEV_ID]}' data[key] = response_data[ATTR_DEVICE_REAL_KPI_DATA_ITEM_MAP] return data def device_ids_grouped_per_type_id(self): device_registry = dr.async_get(self.hass) device_ids_grouped_per_type_id = {} for device in self.devices: # skip devices wherefore no real kpi data is available if device.type_id not in [PARAM_DEVICE_TYPE_ID_STRING_INVERTER, PARAM_DEVICE_TYPE_ID_EMI, PARAM_DEVICE_TYPE_ID_GRID_METER, PARAM_DEVICE_TYPE_ID_RESIDENTIAL_INVERTER, PARAM_DEVICE_TYPE_ID_BATTERY, PARAM_DEVICE_TYPE_ID_POWER_SENSOR, PARAM_DEVICE_TYPE_ID_C_I_UTILITY_ESS]: continue device_from_registry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) if device_from_registry is not None and device_from_registry.disabled: _LOGGER.debug(f'Device {device.name} ({device.device_id}) is disabled by the user.') continue station_from_registry = device_registry.async_get_device(identifiers={(DOMAIN, device.station_code)}) if station_from_registry is not None and station_from_registry.disabled: _LOGGER.debug( f'Device {device.name} ({device.device_id}) linked to a disabled station ({device.station_code}).') continue if device.type_id not in device_ids_grouped_per_type_id: device_ids_grouped_per_type_id[device.type_id] = [] device_ids_grouped_per_type_id[device.type_id].append(str(device.device_id)) return device_ids_grouped_per_type_id @property def counter_limit(self) -> int: return math.ceil(60 / self.update_interval.total_seconds()) + 1 @property def should_skip(self) -> bool: return self.skip and self.skip_counter <= self.counter_limit @property def skip_for(self) -> int: return (self.counter_limit - self.skip_counter + 1) * self.update_interval.total_seconds() ================================================ FILE: custom_components/fusion_solar/fusion_solar/const.py ================================================ # Fusion Solar API response attributes ATTR_AID_TYPE = 'aidType' ATTR_BUILD_STATE = 'buildState' ATTR_CAPACITY = 'capacity' ATTR_COMBINE_TYPE = 'combineType' ATTR_CONTACT_PERSON_PHONE = 'linkmanPho' ATTR_DATA = 'data' ATTR_DATA_COLLECT_TIME = 'collectTime' ATTR_DATA_REALKPI = 'realKpi' ATTR_DEVICE_ESN_CODE = 'esnCode' ATTR_DEVICE_ID = 'id' ATTR_DEVICE_INVERTER_TYPE = 'invType' ATTR_DEVICE_LATITUDE = 'latitude' ATTR_DEVICE_LONGITUDE = 'longitude' ATTR_DEVICE_NAME = 'devName' ATTR_DEVICE_SOFTWARE_VERSION = 'softwareVersion' ATTR_DEVICE_STATION_CODE = 'stationCode' ATTR_DEVICE_TYPE_ID = 'devTypeId' ATTR_FAIL_CODE = 'failCode' ATTR_LIST = 'list' ATTR_MESSAGE = 'message' ATTR_PARAMS = 'params' ATTR_PARAMS_CURRENT_TIME = 'currentTime' ATTR_PLANT_ADDRESS = 'plantAddress' ATTR_PLANT_CODE = 'plantCode' ATTR_PLANT_NAME = 'plantName' ATTR_STATION_ADDRESS = 'stationAddr' ATTR_STATION_CODE = 'stationCode' ATTR_STATION_CONTACT_PERSON = 'contactPerson' ATTR_STATION_LINKMAN = 'stationLinkman' ATTR_STATION_NAME = 'stationName' ATTR_SUCCESS = 'success' # Data attributes ATTR_REALTIME_POWER = 'realTimePower' ATTR_TOTAL_CURRENT_DAY_ENERGY = 'dailyEnergy' ATTR_TOTAL_CURRENT_MONTH_ENERGY = 'monthEnergy' ATTR_TOTAL_CURRENT_YEAR_ENERGY = 'yearEnergy' ATTR_TOTAL_LIFETIME_ENERGY = 'cumulativeEnergy' ATTR_STATION_REAL_KPI_DATA_ITEM_MAP = 'dataItemMap' ATTR_STATION_REAL_KPI_TOTAL_CURRENT_DAY_ENERGY = 'day_power' ATTR_STATION_REAL_KPI_TOTAL_CURRENT_MONTH_ENERGY = 'month_power' ATTR_STATION_REAL_KPI_TOTAL_LIFETIME_ENERGY = 'total_power' ATTR_KPI_YEAR_INVERTER_POWER = 'inverter_power' ATTR_DEVICE_REAL_KPI_ACTIVE_POWER = 'active_power' ATTR_DEVICE_REAL_KPI_DATA_ITEM_MAP = 'dataItemMap' ATTR_DEVICE_REAL_KPI_DEV_ID = 'devId' PARAM_DEVICE_TYPE_ID_BATTERY = 39 PARAM_DEVICE_TYPE_ID_EMI = 10 PARAM_DEVICE_TYPE_ID_GRID_METER = 17 PARAM_DEVICE_TYPE_ID_POWER_SENSOR = 47 PARAM_DEVICE_TYPE_ID_RESIDENTIAL_INVERTER = 38 PARAM_DEVICE_TYPE_ID_C_I_UTILITY_ESS = 41 PARAM_DEVICE_TYPE_ID_STRING_INVERTER = 1 ================================================ FILE: custom_components/fusion_solar/fusion_solar/device_attribute_entity.py ================================================ from homeassistant.helpers.entity import Entity, EntityCategory from .openapi.device import FusionSolarDevice from ..const import DOMAIN class FusionSolarDeviceAttributeEntity(Entity): def __init__( self, device: FusionSolarDevice, name, attribute, value ): """Initialize the entity""" self._device = device self._name = name self._attribute = attribute self._device_info = device.device_info() self._value = value @property def unique_id(self) -> str: return f'{DOMAIN}-{self._device.device_id}-{self._attribute}' @property def name(self): return f'{self._device.readable_name} - {self._name}' @property def state(self): return self._value @property def device_info(self) -> dict: return self._device_info @property def entity_category(self) -> str: return EntityCategory.DIAGNOSTIC @property def should_poll(self) -> bool: return False class FusionSolarDeviceLatitudeEntity(FusionSolarDeviceAttributeEntity): _attr_icon = 'mdi:latitude' class FusionSolarDeviceLongitudeEntity(FusionSolarDeviceAttributeEntity): _attr_icon = 'mdi:longitude' ================================================ FILE: custom_components/fusion_solar/fusion_solar/energy_sensor.py ================================================ import logging import math from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.components.sensor import SensorEntity, SensorDeviceClass, SensorStateClass from homeassistant.const import UnitOfEnergy from .const import ATTR_TOTAL_LIFETIME_ENERGY, ATTR_REALTIME_POWER _LOGGER = logging.getLogger(__name__) class FusionSolarEnergySensor(CoordinatorEntity, SensorEntity): """Base class for all FusionSolarEnergySensor sensors.""" def __init__( self, coordinator, unique_id, name, attribute, data_name, device_info=None ): """Initialize the entity""" super().__init__(coordinator) self._unique_id = unique_id self._name = name self._attribute = attribute self._data_name = data_name self._device_info = device_info @property def device_class(self) -> str: return SensorDeviceClass.ENERGY @property def unique_id(self) -> str: return self._unique_id @property def name(self) -> str: return self._name @property def native_value(self) -> float: # It seems like Huawei Fusion Solar returns some invalid data for the cumulativeEnergy just before midnight. # So we update the value only if the system is producing power at the moment. if ATTR_TOTAL_LIFETIME_ENERGY == self._attribute: # Grab the current data entity = self.hass.states.get(self.entity_id) if entity is not None: try: current_value = float(entity.state) except ValueError: _LOGGER.info(f'{self.entity_id}: not available, so no update to prevent issues.') return # Return the current value if the system is not producing if not self.is_producing_at_the_moment(): _LOGGER.info(f'{self.entity_id}: not producing any power, so no update to prevent glitches.') return current_value try: return self.get_float_value_from_coordinator(self._attribute) except FusionSolarEnergySensorException as e: _LOGGER.error(e) return None @property def native_unit_of_measurement(self) -> str: return UnitOfEnergy.KILO_WATT_HOUR @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING @property def device_info(self) -> dict: return self._device_info def is_producing_at_the_moment(self) -> bool: try: realtime_power = self.get_float_value_from_coordinator(ATTR_REALTIME_POWER) return not math.isclose(realtime_power, 0, abs_tol=0.001) except FusionSolarEnergySensorException as e: _LOGGER.info(e) return False def get_float_value_from_coordinator(self, attribute_name: str) -> float: if self.coordinator.data is False: raise FusionSolarEnergySensorException('Coordinator data is False') if self._data_name not in self.coordinator.data: raise FusionSolarEnergySensorException(f'Attribute {self._data_name} not in coordinator data') if self._attribute not in self.coordinator.data[self._data_name]: raise FusionSolarEnergySensorException(f'Attribute {attribute_name} not in coordinator data') if self.coordinator.data[self._data_name][attribute_name] is None: raise FusionSolarEnergySensorException(f'Attribute {attribute_name} has value None') elif self.coordinator.data[self._data_name][attribute_name] == 'N/A': raise FusionSolarEnergySensorException(f'Attribute {attribute_name} has value N/A') try: return float(self.coordinator.data[self._data_name][attribute_name]) except ValueError: raise FusionSolarEnergySensorException( f'Attribute {self._attribute} has value {self.coordinator.data[self._data_name][attribute_name]} which is not a float') class FusionSolarEnergySensorTotalCurrentDay(FusionSolarEnergySensor): pass class FusionSolarEnergySensorTotalCurrentMonth(FusionSolarEnergySensor): pass class FusionSolarEnergySensorTotalCurrentYear(FusionSolarEnergySensor): pass class FusionSolarEnergySensorTotalLifetime(FusionSolarEnergySensor): pass class FusionSolarEnergySensorException(Exception): pass ================================================ FILE: custom_components/fusion_solar/fusion_solar/kiosk/kiosk.py ================================================ import re import logging from urllib.parse import urlparse _LOGGER = logging.getLogger(__name__) class FusionSolarKiosk: def __init__(self, url, name): self.url = url self.name = name self._parseId() def _parseId(self): id = re.search("\\?kk=(.*)", self.url).group(1) _LOGGER.debug('calculated KioskId: ' + id) self.id = id def apiUrl(self): url = urlparse(self.url) apiUrl = (url.scheme + "://" + url.netloc) _LOGGER.debug('calculated API base url for ' + self.id + ': ' + apiUrl) return apiUrl ================================================ FILE: custom_components/fusion_solar/fusion_solar/kiosk/kiosk_api.py ================================================ """API client for FusionSolar Kiosk.""" import logging import html import json from ..const import ( ATTR_DATA, ATTR_FAIL_CODE, ATTR_SUCCESS, ATTR_DATA_REALKPI, ) from requests import get _LOGGER = logging.getLogger(__name__) class FusionSolarKioskApi: def __init__(self, host): self._host = host def getRealTimeKpi(self, id: str): url = self._host + '/rest/pvms/web/kiosk/v1/station-kiosk-file?kk=' + id headers = { 'accept': 'application/json', } try: response = get(url, headers=headers) jsonData = response.json() if ATTR_SUCCESS not in jsonData or not jsonData[ATTR_SUCCESS]: raise FusionSolarKioskApiError(f'Retrieving the data failed. Raw response: {response.text}') # convert encoded html string to JSON jsonData[ATTR_DATA] = json.loads(html.unescape(jsonData[ATTR_DATA])) _LOGGER.debug('Received data for ' + id + ': ') _LOGGER.debug(jsonData[ATTR_DATA][ATTR_DATA_REALKPI]) return jsonData[ATTR_DATA][ATTR_DATA_REALKPI] except KeyError as error: _LOGGER.error(error) _LOGGER.error(response.text) except FusionSolarKioskApiError as error: _LOGGER.error(error) _LOGGER.debug(response.text) return { ATTR_SUCCESS: False } class FusionSolarKioskApiError(Exception): pass ================================================ FILE: custom_components/fusion_solar/fusion_solar/lifetime_plant_data_entity.py ================================================ import logging from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.components.sensor import SensorEntity, SensorDeviceClass, SensorStateClass from homeassistant.const import UnitOfEnergy, UnitOfMass from ..const import DOMAIN _LOGGER = logging.getLogger(__name__) class FusionSolarLifetimePlantDataSensor(CoordinatorEntity, SensorEntity): """Base class for all FusionSolarLifetimePlantDataSensor sensors.""" def __init__( self, coordinator, station, ): """Initialize the entity""" super().__init__(coordinator) self._station = station self._device_info = station.device_info() @property def unique_id(self) -> str: return f'{DOMAIN}-{self._station.code}-lifetime-{self._attribute}' @property def native_value(self) -> float: key = f'{DOMAIN}-{self._station.code}' if key not in self.coordinator.data: return None total = None for collect_time, data in self.coordinator.data[key].items(): if self._attribute in data and data[self._attribute] is not None: if total is None: total = 0 total = total + float(data[self._attribute]) return total @property def device_info(self) -> dict: return self._device_info class FusionSolarLifetimePlantDataInverterPowerSensor(FusionSolarLifetimePlantDataSensor): _attribute = 'inverter_power' @property def name(self) -> str: return f'{self._station.readable_name} - Lifetime - Inverter yield' @property def device_class(self) -> str: return SensorDeviceClass.ENERGY @property def native_unit_of_measurement(self) -> str: return UnitOfEnergy.KILO_WATT_HOUR @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING class FusionSolarLifetimePlantDataOngridPowerSensor(FusionSolarLifetimePlantDataSensor): _attribute = 'ongrid_power' @property def name(self) -> str: return f'{self._station.readable_name} - Lifetime - Feed-in energy' @property def device_class(self) -> str: return SensorDeviceClass.ENERGY @property def native_unit_of_measurement(self) -> str: return UnitOfEnergy.KILO_WATT_HOUR @property def state_class(self) -> str: return SensorStateClass.TOTAL class FusionSolarLifetimePlantDataUsePowerSensor(FusionSolarLifetimePlantDataSensor): _attribute = 'use_power' @property def name(self) -> str: return f'{self._station.readable_name} - Lifetime - Consumption' @property def device_class(self) -> str: return SensorDeviceClass.ENERGY @property def native_unit_of_measurement(self) -> str: return UnitOfEnergy.KILO_WATT_HOUR @property def state_class(self) -> str: return SensorStateClass.TOTAL class FusionSolarLifetimePlantDataPowerProfitSensor(FusionSolarLifetimePlantDataSensor): _attribute = 'power_profit' @property def name(self) -> str: return f'{self._station.readable_name} - Lifetime - Revenue' @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING @property def icon(self) -> str | None: return "mdi:cash" class FusionSolarLifetimePlantDataPerpowerRatioSensor(FusionSolarLifetimePlantDataSensor): _attribute = 'perpower_ratio' @property def name(self) -> str: return f'{self._station.readable_name} - Lifetime - Specific energy' @property def state_class(self) -> str: return SensorStateClass.TOTAL class FusionSolarLifetimePlantDataReductionTotalCo2Sensor(FusionSolarLifetimePlantDataSensor): _attribute = 'reduction_total_co2' @property def name(self) -> str: return f'{self._station.readable_name} - Lifetime - CO2 emission reduction' @property def native_unit_of_measurement(self) -> str: return UnitOfMass.KILOGRAMS @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING @property def native_value(self) -> float: native_value = super().native_value if native_value is None: return None return native_value * 1000 @property def icon(self) -> str | None: return "mdi:molecule-co2" class FusionSolarLifetimePlantDataReductionTotalCoalSensor(FusionSolarLifetimePlantDataSensor): _attribute = 'reduction_total_coal' @property def name(self) -> str: return f'{self._station.readable_name} - Lifetime - Standard coal saved' @property def native_unit_of_measurement(self) -> str: return UnitOfMass.KILOGRAMS @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING @property def native_value(self) -> float: native_value = super().native_value if native_value is None: return None return native_value * 1000 @property def icon(self) -> str | None: return "mdi:weight" class FusionSolarLifetimePlantDataReductionTotalTreeSensor(FusionSolarLifetimePlantDataSensor): _attribute = 'reduction_total_tree' @property def name(self) -> str: return f'{self._station.readable_name} - Lifetime - Equivalent tree planted' @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING @property def icon(self) -> str | None: return "mdi:tree" ================================================ FILE: custom_components/fusion_solar/fusion_solar/openapi/device.py ================================================ from ...const import DOMAIN class FusionSolarDevice: def __init__( self, device_id: str, name: str, station_code: str, esn_code: str, type_id: str, inverter_type, software_version: str, longitude: float, latitude: float ): self.device_id = device_id self.name = name self.station_code = station_code self.esn_code = esn_code self.type_id = type_id self.inverter_type = inverter_type self.software_version = software_version self.longitude = longitude self.latitude = latitude @property def model(self) -> str: if self.type_id == 38: return f'{self.device_type} {self.inverter_type}' return self.device_type @property def device_type(self) -> str: if self.type_id == 1: return 'String inverter' if self.type_id == 2: return 'SmartLogger' if self.type_id == 8: return 'Transformer' if self.type_id == 10: return 'EMI' if self.type_id == 13: return 'Protocol converter' if self.type_id == 16: return 'General device' if self.type_id == 17: return 'Grid meter' if self.type_id == 22: return 'PID' if self.type_id == 37: return 'Pinnet data logger' if self.type_id == 38: return 'Residential inverter' if self.type_id == 39: return 'Battery' if self.type_id == 40: return 'Backup box' if self.type_id == 41: return 'ESS' if self.type_id == 45: return 'PLC' if self.type_id == 46: return 'Optimizer' if self.type_id == 47: return 'Power Sensor' if self.type_id == 62: return 'Dongle' if self.type_id == 63: return 'Distributed SmartLogger' if self.type_id == 70: return 'Safety box' return 'Unknown' def device_info(self): return { 'identifiers': { (DOMAIN, self.device_id) }, 'name': self.name, 'manufacturer': 'Huawei FusionSolar', 'model': self.model, 'sw_version': self.software_version, 'via_device': (DOMAIN, self.station_code) } @property def readable_name(self): if self.name == self.esn_code: return self.name if self.esn_code is not None and self.esn_code != '': return f'{self.name} ({self.esn_code})' return self.name ================================================ FILE: custom_components/fusion_solar/fusion_solar/openapi/openapi_api.py ================================================ """API client for FusionSolar OpenAPI.""" import logging import time import datetime from datetime import timezone from requests import post from ..const import ATTR_AID_TYPE, ATTR_BUILD_STATE, ATTR_CAPACITY, ATTR_COMBINE_TYPE, ATTR_CONTACT_PERSON_PHONE, \ ATTR_DATA, ATTR_DEVICE_ESN_CODE, ATTR_DEVICE_ID, ATTR_DEVICE_INVERTER_TYPE, ATTR_DEVICE_LATITUDE, \ ATTR_DEVICE_LONGITUDE, ATTR_DEVICE_NAME, ATTR_DEVICE_SOFTWARE_VERSION, ATTR_DEVICE_STATION_CODE, \ ATTR_DEVICE_TYPE_ID, ATTR_FAIL_CODE, ATTR_LIST, ATTR_PARAMS, ATTR_PARAMS_CURRENT_TIME, ATTR_PLANT_ADDRESS, \ ATTR_PLANT_CODE, ATTR_PLANT_NAME, ATTR_STATION_ADDRESS, ATTR_STATION_CODE, ATTR_STATION_CONTACT_PERSON, \ ATTR_STATION_LINKMAN, ATTR_STATION_NAME, ATTR_MESSAGE from .station import FusionSolarStation from .device import FusionSolarDevice _LOGGER = logging.getLogger(__name__) class FusionSolarOpenApi: def __init__(self, host: str, username: str, password: str): self._token = None self._host = host self._username = username self._password = password def login(self) -> str: url = self._host + '/thirdData/login' headers = { 'accept': 'application/json', } json = { 'userName': self._username, 'systemCode': self._password, } try: response = post(url, headers=headers, json=json) response.raise_for_status() if 'xsrf-token' in response.headers: self._token = response.headers['xsrf-token'] return response.headers.get("xsrf-token") raise FusionSolarOpenApiError(f'Could not login with given credentials') except Exception as error: raise FusionSolarOpenApiError(f'Could not login with given credentials') def get_station_list(self): url = self._host + '/thirdData/getStationList' json = {} try: response = self._do_call(url, json) except FusionSolarOpenApiErrorInvalidAccessToCurrentInterfaceError as error: _LOGGER.debug(f'Could not use getStationList, trying stations: {error}') return self.stations() data = [] for station in response[ATTR_DATA]: data.append( FusionSolarStation( station[ATTR_STATION_CODE], station[ATTR_STATION_NAME], station[ATTR_STATION_ADDRESS], station[ATTR_CAPACITY], station[ATTR_BUILD_STATE], station[ATTR_COMBINE_TYPE], station[ATTR_AID_TYPE], station[ATTR_STATION_LINKMAN], station[ATTR_CONTACT_PERSON_PHONE] ) ) return data def stations(self): url = self._host + '/thirdData/stations' json = { 'pageNo': 1, } response = self._do_call(url, json) data = [] for station in response[ATTR_DATA][ATTR_LIST]: data.append( FusionSolarStation( station[ATTR_PLANT_CODE], station[ATTR_PLANT_NAME], station[ATTR_PLANT_ADDRESS], station[ATTR_CAPACITY], None, None, None, station[ATTR_STATION_CONTACT_PERSON], None ) ) return data def get_station_real_kpi(self, station_codes: list): url = self._host + '/thirdData/getStationRealKpi' json = { 'stationCodes': ','.join(station_codes), } response = self._do_call(url, json) return response[ATTR_DATA] def get_kpi_station_year(self, station_codes: list): today = datetime.datetime.now() next_year = datetime.datetime(year=today.year + 1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc) url = self._host + '/thirdData/getKpiStationYear' json = { 'stationCodes': ','.join(station_codes), 'collectTime': round(next_year.timestamp() * 1000), } response = self._do_call(url, json) return response[ATTR_DATA] def get_dev_list(self, station_codes: list): url = self._host + '/thirdData/getDevList' json = { 'stationCodes': ','.join(station_codes), } response = self._do_call(url, json) data = [] for device in response[ATTR_DATA]: data.append( FusionSolarDevice( device[ATTR_DEVICE_ID], device[ATTR_DEVICE_NAME], device[ATTR_DEVICE_STATION_CODE], device[ATTR_DEVICE_ESN_CODE], device[ATTR_DEVICE_TYPE_ID], device[ATTR_DEVICE_INVERTER_TYPE], device[ATTR_DEVICE_SOFTWARE_VERSION], device[ATTR_DEVICE_LATITUDE], device[ATTR_DEVICE_LONGITUDE], ) ) return data def get_dev_real_kpi(self, device_ids: list, type_id: int): url = self._host + '/thirdData/getDevRealKpi' json = { 'devIds': ','.join(device_ids), 'devTypeId': type_id, } response = self._do_call(url, json) return response[ATTR_DATA] def _do_call(self, url: str, json: dict): if self._token is None: self.login() headers = { 'accept': 'application/json', 'xsrf-token': self._token, } try: response = post(url, headers=headers, json=json) response.raise_for_status() json_data = response.json() _LOGGER.debug(f'JSON data for {url}: {json_data}') if ATTR_FAIL_CODE in json_data and json_data[ATTR_FAIL_CODE] == 305: _LOGGER.debug('Token expired, trying to login again') # token expired self._token = None return self._do_call(url, json) if ATTR_FAIL_CODE in json_data and json_data[ATTR_FAIL_CODE] == 401: raise FusionSolarOpenApiErrorInvalidAccessToCurrentInterfaceError(json_data[ATTR_MESSAGE]) if ATTR_FAIL_CODE in json_data and json_data[ATTR_FAIL_CODE] == 407: _LOGGER.debug( f'Access frequency to high, while calling {url}: {json_data[ATTR_DATA]}, failcode: {json_data[ATTR_FAIL_CODE]}') raise FusionSolarOpenApiAccessFrequencyTooHighError( f'Access frequency to high. failCode: {json_data[ATTR_FAIL_CODE]}, message: {json_data[ATTR_DATA]}' ) if ATTR_FAIL_CODE in json_data and json_data[ATTR_FAIL_CODE] != 0: _LOGGER.debug(f'Error calling {url}: {json_data[ATTR_DATA]}, failcode: {json_data[ATTR_FAIL_CODE]}') raise FusionSolarOpenApiError( f'Retrieving the data for {url} failed with failCode: {json_data[ATTR_FAIL_CODE]}, message: {json_data[ATTR_DATA]}' ) if ATTR_DATA not in json_data: raise FusionSolarOpenApiError(f'Retrieving the data failed. Raw response: {response.text}') return json_data except KeyError as error: _LOGGER.error(error) _LOGGER.error(response.text) class FusionSolarOpenApiError(Exception): pass class FusionSolarOpenApiAccessFrequencyTooHighError(FusionSolarOpenApiError): pass class FusionSolarOpenApiErrorInvalidAccessToCurrentInterfaceError(FusionSolarOpenApiError): pass ================================================ FILE: custom_components/fusion_solar/fusion_solar/openapi/station.py ================================================ from ...const import DOMAIN class FusionSolarStation: def __init__( self, code: str, name: str, address: str = None, capacity: float = None, build_state: str = None, combine_type: str = None, aid_type: int = None, contact_person: str = None, contact_phone: str = None ): self.code = code self.name = name self.address = address self.capacity = capacity self.build_state = build_state self.combine_type = combine_type self.aid_type = aid_type self.contact_person = contact_person self.contact_phone = contact_phone def device_info(self): return { 'identifiers': { (DOMAIN, self.code) }, 'name': self.name, 'manufacturer': 'Huawei FusionSolar', 'model': 'Station' } @property def readable_name(self): if self.name is not None and self.name != '': return self.name return self.code ================================================ FILE: custom_components/fusion_solar/fusion_solar/power_entity.py ================================================ from homeassistant.core import callback from homeassistant.const import UnitOfPower from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.entity import Entity class FusionSolarPowerEntity(CoordinatorEntity, Entity): """Base class for all FusionSolarPowerEntity entities.""" def __init__( self, coordinator, unique_id, name, attribute, data_name, device_info=None ): """Initialize the entity""" super().__init__(coordinator) self._unique_id = unique_id self._name = name self._attribute = attribute self._data_name = data_name self._device_info = device_info self._state = '__NOT_INITIALIZED__' @property def device_class(self): return SensorDeviceClass.POWER @property def unique_id(self) -> str: return self._unique_id @property def name(self): return self._name @property def state(self): if self._state == '__NOT_INITIALIZED__': # check if data is available self._handle_coordinator_update() if self._state is None or self._state == '__NOT_INITIALIZED__': return None return self._state @property def unit_of_measurement(self): return UnitOfPower.KILO_WATT @property def device_info(self) -> dict: return self._device_info @callback def _handle_coordinator_update(self): if self.coordinator.data is False: return if self._data_name not in self.coordinator.data: return if self._attribute not in self.coordinator.data[self._data_name]: return if self.coordinator.data[self._data_name][self._attribute] is None: self._state = None elif self.coordinator.data[self._data_name][self._attribute] == 'N/A': self._state = None else: self._state = float(self.coordinator.data[self._data_name][self._attribute]) self.async_write_ha_state() class FusionSolarPowerEntityRealtime(FusionSolarPowerEntity): pass class FusionSolarPowerEntityRealtimeInWatt(FusionSolarPowerEntity): @property def unit_of_measurement(self): return UnitOfPower.WATT ================================================ FILE: custom_components/fusion_solar/fusion_solar/realtime_device_data_sensor.py ================================================ import datetime from homeassistant.core import callback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.components.sensor import SensorEntity, SensorStateClass, SensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorDeviceClass from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfPower, UnitOfTemperature, UnitOfElectricCurrent, \ UnitOfElectricPotential, UnitOfFrequency from .openapi.device import FusionSolarDevice from ..const import DOMAIN class FusionSolarRealtimeDeviceDataSensor(CoordinatorEntity, SensorEntity): """Base class for all FusionSolarRealtimeDeviceDataSensor sensors.""" def __init__( self, coordinator, device: FusionSolarDevice, name: str, attribute: str ): """Initialize the entity""" super().__init__(coordinator) self._device = device self._name = name self._attribute = attribute self._data_name = f'{DOMAIN}-{device.device_id}' self._device_info = device.device_info() self._state = '__NOT_INITIALIZED__' @property def unique_id(self) -> str: return f'{DOMAIN}-{self._device.device_id}-{self._attribute}' @property def name(self) -> str: return f'{self._device.name} - {self._name}' @property def native_value(self) -> float: if self._state == '__NOT_INITIALIZED__': # check if data is available self._handle_coordinator_update() if self._state is None or self._state == '__NOT_INITIALIZED__': return None return float(self._state) @property def device_info(self) -> dict: return self._device_info @callback def _handle_coordinator_update(self): if self.coordinator.data is False: return if self._data_name not in self.coordinator.data: return if self._attribute not in self.coordinator.data[self._data_name]: return if self.coordinator.data[self._data_name][self._attribute] is None: self._state = None elif self.coordinator.data[self._data_name][self._attribute] == 'N/A': self._state = None else: self._state = float(self.coordinator.data[self._data_name][self._attribute]) self.async_write_ha_state() class FusionSolarRealtimeDeviceDataTranslatedSensor(FusionSolarRealtimeDeviceDataSensor): @property def state(self) -> int: if self._state == '__NOT_INITIALIZED__': # check if data is available self._handle_coordinator_update() if self._state is None or self._state == '__NOT_INITIALIZED__': return None return int(self._state) @property def translation_key(self) -> str: return self._attribute class FusionSolarRealtimeDeviceDataVoltageSensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return SensorDeviceClass.VOLTAGE @property def native_unit_of_measurement(self) -> str: return UnitOfElectricPotential.VOLT @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT class FusionSolarRealtimeDeviceDataCurrentSensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return SensorDeviceClass.CURRENT @property def native_unit_of_measurement(self) -> str: return UnitOfElectricCurrent.AMPERE @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT class FusionSolarRealtimeDeviceDataEnergySensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return SensorDeviceClass.ENERGY @property def native_unit_of_measurement(self) -> str: return UnitOfEnergy.KILO_WATT_HOUR @property def state_class(self) -> str: return SensorStateClass.TOTAL class FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor(FusionSolarRealtimeDeviceDataEnergySensor): @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING class FusionSolarRealtimeDeviceDataTemperatureSensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return SensorDeviceClass.TEMPERATURE @property def native_unit_of_measurement(self) -> str: return UnitOfTemperature.CELSIUS @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT class FusionSolarRealtimeDeviceDataPowerFactorSensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return SensorDeviceClass.POWER_FACTOR @property def native_unit_of_measurement(self) -> str: return PERCENTAGE @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT @property def native_value(self) -> str: native_value = super().native_value if native_value is None: return None return native_value * 100 class FusionSolarRealtimeDeviceDataFrequencySensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return SensorDeviceClass.FREQUENCY @property def native_unit_of_measurement(self) -> str: return UnitOfFrequency.HERTZ @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT class FusionSolarRealtimeDeviceDataPowerSensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return SensorDeviceClass.POWER @property def native_unit_of_measurement(self) -> str: return UnitOfPower.KILO_WATT @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT class FusionSolarRealtimeDeviceDataPowerInWattSensor(FusionSolarRealtimeDeviceDataPowerSensor): @property def native_unit_of_measurement(self) -> str: return UnitOfPower.WATT class FusionSolarRealtimeDeviceDataReactivePowerSensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return SensorDeviceClass.REACTIVE_POWER @property def native_unit_of_measurement(self) -> str: return 'var' @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT @property def native_value(self) -> float: native_value = super().native_value if native_value is None: return None return native_value * 1000 class FusionSolarRealtimeDeviceDataReactivePowerInVarSensor(FusionSolarRealtimeDeviceDataReactivePowerSensor): @property def native_unit_of_measurement(self) -> str: return 'var' class FusionSolarRealtimeDeviceDataApparentPowerSensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return 'apparent_power' @property def native_unit_of_measurement(self) -> str: return 'kVA' @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT class FusionSolarRealtimeDeviceDataWindSpeedSensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return 'wind_speed' @property def native_unit_of_measurement(self) -> str: return 'm/s' @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT class FusionSolarRealtimeDeviceDataBatterySensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return SensorDeviceClass.BATTERY @property def native_unit_of_measurement(self) -> str: return PERCENTAGE @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT class FusionSolarRealtimeDeviceDataTimestampSensor(FusionSolarRealtimeDeviceDataSensor): @property def device_class(self) -> str: return SensorDeviceClass.TIMESTAMP @property def state(self) -> datetime: state = super().native_value if state is None: return None return datetime.datetime.fromtimestamp(state / 1000) class FusionSolarRealtimeDeviceDataPercentageSensor(FusionSolarRealtimeDeviceDataSensor): @property def native_unit_of_measurement(self) -> str | None: return PERCENTAGE @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT class FusionSolarRealtimeDeviceDataBinarySensor(CoordinatorEntity, BinarySensorEntity): """Base class for all FusionSolarRealtimeDeviceDataBinarySensor sensors.""" def __init__( self, coordinator, device: FusionSolarDevice, name: str, attribute: str ): """Initialize the entity""" super().__init__(coordinator) self._device = device self._name = name self._attribute = attribute self._data_name = f'{DOMAIN}-{device.device_id}' self._device_info = device.device_info() self._state = '__NOT_INITIALIZED__' @property def unique_id(self) -> str: return f'{DOMAIN}-{self._device.device_id}-{self._attribute}' @property def name(self) -> str: return f'{self._device.name} - {self._name}' @property def device_info(self) -> dict: return self._device_info @callback def _handle_coordinator_update(self): if self.coordinator.data is False: return if self._data_name not in self.coordinator.data: return if self._attribute not in self.coordinator.data[self._data_name]: return if self.coordinator.data[self._data_name][self._attribute] is None: self._state = None else: self._state = float(self.coordinator.data[self._data_name][self._attribute]) self.async_write_ha_state() class FusionSolarRealtimeDeviceDataStateBinarySensor(FusionSolarRealtimeDeviceDataBinarySensor): @property def device_class(self) -> str: return BinarySensorDeviceClass.CONNECTIVITY @property def is_on(self) -> bool: if self._state == '__NOT_INITIALIZED__': # check if data is available self._handle_coordinator_update() if self._state is None or self._state == '__NOT_INITIALIZED__': return None if self._state == 0: return False if self._state == 1: return True return None ================================================ FILE: custom_components/fusion_solar/fusion_solar/station_attribute_entity.py ================================================ from homeassistant.helpers.entity import Entity, EntityCategory from .openapi.station import FusionSolarStation from ..const import DOMAIN class FusionSolarStationAttributeEntity(Entity): def __init__( self, station: FusionSolarStation, name, attribute, value ): """Initialize the entity""" self._station = station self._name = name self._attribute = attribute self._device_info = station.device_info() self._value = value @property def unique_id(self) -> str: return f'{DOMAIN}-{self._station.code}-{self._attribute}' @property def name(self): return f'{self._station.readable_name} - {self._name}' @property def state(self): return self._value @property def device_info(self) -> dict: return self._device_info @property def entity_category(self) -> str: return EntityCategory.DIAGNOSTIC @property def should_poll(self) -> bool: return False class FusionSolarStationCapacityEntity(FusionSolarStationAttributeEntity): _attr_icon = 'mdi:lightning-bolt' class FusionSolarStationContactPersonEntity(FusionSolarStationAttributeEntity): _attr_icon = 'mdi:account' class FusionSolarStationContactPersonPhoneEntity(FusionSolarStationAttributeEntity): _attr_icon = 'mdi:card-account-phone' class FusionSolarStationAddressEntity(FusionSolarStationAttributeEntity): _attr_icon = 'mdi:map-marker' ================================================ FILE: custom_components/fusion_solar/fusion_solar/year_plant_data_entity.py ================================================ import logging from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.components.sensor import SensorEntity, SensorDeviceClass, SensorStateClass from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfMass, UnitOfIrradiance from ..const import DOMAIN _LOGGER = logging.getLogger(__name__) class FusionSolarYearPlantDataSensor(CoordinatorEntity, SensorEntity): """Base class for all FusionSolarYearPlantDataSensor sensors.""" def __init__( self, coordinator, station, ): """Initialize the entity""" super().__init__(coordinator) self._station = station self._device_info = station.device_info() @property def unique_id(self) -> str: return f'{DOMAIN}-{self._station.code}-current_year-{self._attribute}' @property def native_value(self) -> float: key = f'{DOMAIN}-{self._station.code}' if key not in self.coordinator.data: return None highest_collect_time = 0 latest_data = None for collect_time, data in self.coordinator.data[key].items(): if collect_time > highest_collect_time: highest_collect_time = collect_time latest_data = data if self._attribute not in latest_data: return None if latest_data[self._attribute] is None: return None return float(latest_data[self._attribute]) @property def device_info(self) -> dict: return self._device_info class FusionSolarYearPlantDataInstalledCapacitySensor(FusionSolarYearPlantDataSensor): _attribute = 'installed_capacity' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Installed capacity' @property def device_class(self) -> str: return SensorDeviceClass.POWER @property def native_unit_of_measurement(self) -> str: return UnitOfPower.KILO_WATT @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT class FusionSolarYearPlantDataRadiationIntensitySensor(FusionSolarYearPlantDataSensor): _attribute = 'radiation_intensity' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Global irradiation' @property def device_class(self) -> str: return SensorDeviceClass.IRRADIANCE @property def native_unit_of_measurement(self) -> str: return UnitOfIrradiance.WATTS_PER_SQUARE_METER @property def state_class(self) -> str: return SensorStateClass.MEASUREMENT @property def state(self) -> float: super_state = super().state if super_state is None: return None return super_state * 1000 class FusionSolarYearPlantDataTheoryPowerSensor(FusionSolarYearPlantDataSensor): _attribute = 'theory_power' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Theoretical yield' @property def device_class(self) -> str: return SensorDeviceClass.ENERGY @property def native_unit_of_measurement(self) -> str: return UnitOfEnergy.KILO_WATT_HOUR @property def state_class(self) -> str: return SensorStateClass.TOTAL class FusionSolarYearPlantDataPerformanceRatioSensor(FusionSolarYearPlantDataSensor): _attribute = 'performance_ratio' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Performance ratio' @property def device_class(self) -> str: return SensorDeviceClass.ENERGY @property def native_unit_of_measurement(self) -> str: return UnitOfEnergy.KILO_WATT_HOUR @property def state_class(self) -> str: return SensorStateClass.TOTAL class FusionSolarYearPlantDataInverterPowerSensor(FusionSolarYearPlantDataSensor): _attribute = 'inverter_power' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Inverter yield' @property def device_class(self) -> str: return SensorDeviceClass.ENERGY @property def native_unit_of_measurement(self) -> str: return UnitOfEnergy.KILO_WATT_HOUR @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING class FusionSolarYearPlantDataOngridPowerSensor(FusionSolarYearPlantDataSensor): _attribute = 'ongrid_power' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Feed-in energy' @property def device_class(self) -> str: return SensorDeviceClass.ENERGY @property def native_unit_of_measurement(self) -> str: return UnitOfEnergy.KILO_WATT_HOUR @property def state_class(self) -> str: return SensorStateClass.TOTAL class FusionSolarYearPlantDataUsePowerSensor(FusionSolarYearPlantDataSensor): _attribute = 'use_power' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Consumption' @property def device_class(self) -> str: return SensorDeviceClass.ENERGY @property def native_unit_of_measurement(self) -> str: return UnitOfEnergy.KILO_WATT_HOUR @property def state_class(self) -> str: return SensorStateClass.TOTAL class FusionSolarYearPlantDataPowerProfitSensor(FusionSolarYearPlantDataSensor): _attribute = 'power_profit' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Revenue' @property def device_class(self) -> str: return SensorDeviceClass.MONETARY @property def state_class(self) -> str: return SensorStateClass.TOTAL class FusionSolarYearPlantDataPerpowerRatioSensor(FusionSolarYearPlantDataSensor): _attribute = 'perpower_ratio' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Specific energy' @property def state_class(self) -> str: return SensorStateClass.TOTAL class FusionSolarYearPlantDataReductionTotalCo2Sensor(FusionSolarYearPlantDataSensor): _attribute = 'reduction_total_co2' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - CO2 emission reduction' @property def native_unit_of_measurement(self) -> str: return UnitOfMass.KILOGRAMS @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING @property def native_value(self) -> float: native_value = super().native_value if native_value is None: return None return native_value * 1000 @property def icon(self) -> str | None: return "mdi:molecule-co2" class FusionSolarYearPlantDataReductionTotalCoalSensor(FusionSolarYearPlantDataSensor): _attribute = 'reduction_total_coal' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Standard coal saved' @property def native_unit_of_measurement(self) -> str: return UnitOfMass.KILOGRAMS @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING @property def native_value(self) -> float: native_value = super().native_value if native_value is None: return None return native_value * 1000 @property def icon(self) -> str | None: return "mdi:weight" class FusionSolarYearPlantDataReductionTotalTreeSensor(FusionSolarYearPlantDataSensor): _attribute = 'reduction_total_tree' @property def name(self) -> str: return f'{self._station.readable_name} - Current Year - Equivalent tree planted' @property def state_class(self) -> str: return SensorStateClass.TOTAL_INCREASING @property def icon(self) -> str | None: return "mdi:tree" # @deprecated, use FusionSolarYearPlantDataInverterPowerSensor instead class FusionSolarBackwardsCompatibilityTotalCurrentYear(FusionSolarYearPlantDataInverterPowerSensor): @property def unique_id(self) -> str: return f'{DOMAIN}-{self._station.code}-total_current_year_energy' @property def name(self) -> str: return f'{self._station.readable_name} - Total Current Year Energy' ================================================ FILE: custom_components/fusion_solar/manifest.json ================================================ { "domain": "fusion_solar", "name": "FusionSolar", "codeowners": [ "@tijsVerkoyen" ], "config_flow": true, "dependencies": [], "documentation": "https://github.com/tijsverkoyen/HomeAssistant-FusionSolar", "integration_type": "hub", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/tijsverkoyen/HomeAssistant-FusionSolar/issues", "requirements": [], "version": "3.5.0" } ================================================ FILE: custom_components/fusion_solar/sensor.py ================================================ """FusionSolar sensor.""" import homeassistant.helpers.config_validation as cv import logging import voluptuous as vol from datetime import timedelta from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_URL, CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.exceptions import IntegrationError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .fusion_solar.const import ATTR_REALTIME_POWER, ATTR_TOTAL_CURRENT_DAY_ENERGY, \ ATTR_TOTAL_CURRENT_MONTH_ENERGY, ATTR_TOTAL_CURRENT_YEAR_ENERGY, ATTR_TOTAL_LIFETIME_ENERGY, \ ATTR_STATION_CODE, ATTR_STATION_REAL_KPI_DATA_ITEM_MAP, ATTR_STATION_REAL_KPI_TOTAL_CURRENT_DAY_ENERGY, \ ATTR_STATION_REAL_KPI_TOTAL_CURRENT_MONTH_ENERGY, ATTR_STATION_REAL_KPI_TOTAL_LIFETIME_ENERGY, \ ATTR_DATA_COLLECT_TIME, ATTR_KPI_YEAR_INVERTER_POWER, ATTR_DEVICE_REAL_KPI_ACTIVE_POWER, \ PARAM_DEVICE_TYPE_ID_STRING_INVERTER, PARAM_DEVICE_TYPE_ID_GRID_METER, PARAM_DEVICE_TYPE_ID_RESIDENTIAL_INVERTER, \ PARAM_DEVICE_TYPE_ID_POWER_SENSOR, PARAM_DEVICE_TYPE_ID_EMI, PARAM_DEVICE_TYPE_ID_BATTERY, PARAM_DEVICE_TYPE_ID_C_I_UTILITY_ESS from .fusion_solar.kiosk.kiosk import FusionSolarKiosk from .fusion_solar.kiosk.kiosk_api import FusionSolarKioskApi, FusionSolarKioskApiError from .fusion_solar.openapi.openapi_api import FusionSolarOpenApi, FusionSolarOpenApiError from .fusion_solar.energy_sensor import FusionSolarEnergySensorTotalCurrentDay, \ FusionSolarEnergySensorTotalCurrentMonth, FusionSolarEnergySensorTotalCurrentYear, \ FusionSolarEnergySensorTotalLifetime from .fusion_solar.power_entity import FusionSolarPowerEntityRealtime, FusionSolarPowerEntityRealtimeInWatt from .fusion_solar.device_attribute_entity import * from .fusion_solar.realtime_device_data_sensor import * from .fusion_solar.station_attribute_entity import * from .fusion_solar.year_plant_data_entity import * from .fusion_solar.lifetime_plant_data_entity import * from .device_real_kpi_coordinator import DeviceRealKpiDataCoordinator from .const import CONF_KIOSKS, CONF_OPENAPI_CREDENTIALS, DOMAIN, ID_REALTIME_POWER, NAME_REALTIME_POWER, \ ID_TOTAL_CURRENT_DAY_ENERGY, NAME_TOTAL_CURRENT_DAY_ENERGY, \ ID_TOTAL_CURRENT_MONTH_ENERGY, NAME_TOTAL_CURRENT_MONTH_ENERGY, \ ID_TOTAL_CURRENT_YEAR_ENERGY, NAME_TOTAL_CURRENT_YEAR_ENERGY, \ ID_TOTAL_LIFETIME_ENERGY, NAME_TOTAL_LIFETIME_ENERGY KIOSK_SCHEMA = vol.Schema( { vol.Required(CONF_URL): cv.string, vol.Required(CONF_NAME): cv.string } ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_KIOSKS): vol.All(cv.ensure_list, [KIOSK_SCHEMA]), } ) _LOGGER = logging.getLogger(__name__) def filter_for_enabled_stations(station, device_registry): device_from_registry = device_registry.async_get_device(identifiers={(DOMAIN, station.code)}) if device_from_registry is not None and device_from_registry.disabled: _LOGGER.debug(f'Station {station.code} is disabled by the user.') return False return True async def add_entities_for_kiosk(hass, async_add_entities, kiosk: FusionSolarKiosk): _LOGGER.debug(f'Adding entities for kiosk {kiosk.id}') async def async_update_kiosk_data(): """Fetch data""" data = {} api = FusionSolarKioskApi(kiosk.apiUrl()) _LOGGER.debug(DOMAIN) _LOGGER.debug(kiosk.id) try: data[f'{DOMAIN}-{kiosk.id}'] = await hass.async_add_executor_job(api.getRealTimeKpi, kiosk.id) except FusionSolarKioskApiError as error: raise UpdateFailed(f'Kiosk API Error: {error}') return data coordinator = DataUpdateCoordinator( hass, _LOGGER, name='FusionSolarKiosk', update_method=async_update_kiosk_data, update_interval=timedelta(seconds=600), ) # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() device_info = { 'identifiers': { (DOMAIN, kiosk.id) }, 'name': kiosk.name, 'manufacturer': 'Huawei FusionSolar', 'model': 'Kiosk' } async_add_entities([ FusionSolarPowerEntityRealtime( coordinator, f'{DOMAIN}-{kiosk.id}-{ID_REALTIME_POWER}', f'{kiosk.name} ({kiosk.id}) - {NAME_REALTIME_POWER}', ATTR_REALTIME_POWER, f'{DOMAIN}-{kiosk.id}', device_info ), FusionSolarEnergySensorTotalCurrentDay( coordinator, f'{DOMAIN}-{kiosk.id}-{ID_TOTAL_CURRENT_DAY_ENERGY}', f'{kiosk.name} ({kiosk.id}) - {NAME_TOTAL_CURRENT_DAY_ENERGY}', ATTR_TOTAL_CURRENT_DAY_ENERGY, f'{DOMAIN}-{kiosk.id}', device_info ), FusionSolarEnergySensorTotalCurrentMonth( coordinator, f'{DOMAIN}-{kiosk.id}-{ID_TOTAL_CURRENT_MONTH_ENERGY}', f'{kiosk.name} ({kiosk.id}) - {NAME_TOTAL_CURRENT_MONTH_ENERGY}', ATTR_TOTAL_CURRENT_MONTH_ENERGY, f'{DOMAIN}-{kiosk.id}', device_info ), FusionSolarEnergySensorTotalCurrentYear( coordinator, f'{DOMAIN}-{kiosk.id}-{ID_TOTAL_CURRENT_YEAR_ENERGY}', f'{kiosk.name} ({kiosk.id}) - {NAME_TOTAL_CURRENT_YEAR_ENERGY}', ATTR_TOTAL_CURRENT_YEAR_ENERGY, f'{DOMAIN}-{kiosk.id}', device_info ), FusionSolarEnergySensorTotalLifetime( coordinator, f'{DOMAIN}-{kiosk.id}-{ID_TOTAL_LIFETIME_ENERGY}', f'{kiosk.name} ({kiosk.id}) - {NAME_TOTAL_LIFETIME_ENERGY}', ATTR_TOTAL_LIFETIME_ENERGY, f'{DOMAIN}-{kiosk.id}', device_info ) ]) async def add_entities_for_stations(hass, async_add_entities, stations, api: FusionSolarOpenApi): device_registry = dr.async_get(hass) stations = list(filter(lambda x: filter_for_enabled_stations(x, device_registry), stations)) station_codes = [station.code for station in stations] _LOGGER.debug(f'Adding entities for stations ({len(station_codes)})') await _add_entities_for_stations_real_kpi_data(hass, async_add_entities, stations, api) await _add_entities_for_stations_year_kpi_data(hass, async_add_entities, stations, api) devices = await hass.async_add_executor_job(api.get_dev_list, station_codes) devices_grouped_per_type_id = {} for device in devices: if device.type_id not in [PARAM_DEVICE_TYPE_ID_STRING_INVERTER, PARAM_DEVICE_TYPE_ID_EMI, PARAM_DEVICE_TYPE_ID_GRID_METER, PARAM_DEVICE_TYPE_ID_RESIDENTIAL_INVERTER, PARAM_DEVICE_TYPE_ID_BATTERY, PARAM_DEVICE_TYPE_ID_POWER_SENSOR]: continue if device.type_id not in devices_grouped_per_type_id: devices_grouped_per_type_id[device.type_id] = [] devices_grouped_per_type_id[device.type_id].append(str(device.device_id)) await _add_static_entities_for_devices(async_add_entities, devices) coordinator = DeviceRealKpiDataCoordinator(hass, api, devices) # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() for device in devices: if device.type_id in [PARAM_DEVICE_TYPE_ID_STRING_INVERTER, PARAM_DEVICE_TYPE_ID_GRID_METER, PARAM_DEVICE_TYPE_ID_RESIDENTIAL_INVERTER]: async_add_entities([ FusionSolarPowerEntityRealtime( coordinator, f'{DOMAIN}-{device.device_id}-{ID_REALTIME_POWER}', f'{device.readable_name} - {NAME_REALTIME_POWER}', ATTR_DEVICE_REAL_KPI_ACTIVE_POWER, f'{DOMAIN}-{device.device_id}', device.device_info() ), ]) if device.type_id in [PARAM_DEVICE_TYPE_ID_POWER_SENSOR]: async_add_entities([ FusionSolarPowerEntityRealtimeInWatt( coordinator, f'{DOMAIN}-{device.device_id}-{ID_REALTIME_POWER}', f'{device.readable_name} - {NAME_REALTIME_POWER}', ATTR_DEVICE_REAL_KPI_ACTIVE_POWER, f'{DOMAIN}-{device.device_id}', device.device_info() ), ]) entities_to_create = [] if device.type_id == PARAM_DEVICE_TYPE_ID_STRING_INVERTER: entities_to_create = [ {'class': 'FusionSolarRealtimeDeviceDataTranslatedSensor', 'attribute': 'inverter_state', 'name': 'Inverter status'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'ab_u', 'name': 'Grid AB voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'bc_u', 'name': 'Grid BC voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'ca_u', 'name': 'Grid CA voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'a_u', 'name': 'Phase A voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'b_u', 'name': 'Phase B voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'c_u', 'name': 'Phase C voltage'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'a_i', 'name': 'Phase A current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'b_i', 'name': 'Phase B current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'c_i', 'name': 'Phase C current'}, {'class': 'FusionSolarRealtimeDeviceDataPercentageSensor', 'attribute': 'efficiency', 'name': 'Inverter efficiency % (manufacturer)'}, {'class': 'FusionSolarRealtimeDeviceDataTemperatureSensor', 'attribute': 'temperature', 'name': 'Inverter internal temperature'}, {'class': 'FusionSolarRealtimeDeviceDataPowerFactorSensor', 'attribute': 'power_factor', 'name': 'Power factor'}, {'class': 'FusionSolarRealtimeDeviceDataFrequencySensor', 'attribute': 'elec_freq', 'name': 'Grid frequency'}, {'class': 'FusionSolarRealtimeDeviceDataPowerSensor', 'attribute': 'active_power', 'name': 'Active power'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reactive_power', 'name': 'Reactive output power'}, {'class': 'FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor', 'attribute': 'day_cap', 'name': 'Yield Today'}, {'class': 'FusionSolarRealtimeDeviceDataPowerSensor', 'attribute': 'mppt_power', 'name': 'MPPT total input power'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv1_u', 'name': 'PV1 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv2_u', 'name': 'PV2 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv3_u', 'name': 'PV3 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv4_u', 'name': 'PV4 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv5_u', 'name': 'PV5 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv6_u', 'name': 'PV6 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv7_u', 'name': 'PV7 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv8_u', 'name': 'PV8 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv9_u', 'name': 'PV9 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv10_u', 'name': 'PV10 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv11_u', 'name': 'PV11 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv12_u', 'name': 'PV12 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv13_u', 'name': 'PV13 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv14_u', 'name': 'PV14 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv15_u', 'name': 'PV15 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv16_u', 'name': 'PV16 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv17_u', 'name': 'PV17 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv18_u', 'name': 'PV18 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv19_u', 'name': 'PV19 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv20_u', 'name': 'PV20 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv21_u', 'name': 'PV21 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv22_u', 'name': 'PV22 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv23_u', 'name': 'PV23 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv24_u', 'name': 'PV24 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv1_i', 'name': 'PV1 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv2_i', 'name': 'PV2 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv3_i', 'name': 'PV3 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv4_i', 'name': 'PV4 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv5_i', 'name': 'PV5 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv6_i', 'name': 'PV6 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv7_i', 'name': 'PV7 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv8_i', 'name': 'PV8 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv9_i', 'name': 'PV9 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv10_i', 'name': 'PV10 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv11_i', 'name': 'PV11 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv12_i', 'name': 'PV12 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv13_i', 'name': 'PV13 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv14_i', 'name': 'PV14 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv15_i', 'name': 'PV15 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv16_i', 'name': 'PV16 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv17_i', 'name': 'PV17 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv18_i', 'name': 'PV18 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv19_i', 'name': 'PV19 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv20_i', 'name': 'PV20 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv21_i', 'name': 'PV21 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv22_i', 'name': 'PV22 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv23_i', 'name': 'PV23 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv24_i', 'name': 'PV24 input current'}, {'class': 'FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor', 'attribute': 'total_cap', 'name': 'Total yield'}, {'class': 'FusionSolarRealtimeDeviceDataTimestampSensor', 'attribute': 'open_time', 'name': 'Inverter startup time'}, {'class': 'FusionSolarRealtimeDeviceDataTimestampSensor', 'attribute': 'close_time', 'name': 'Inverter shutdown time'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_1_cap', 'name': 'MPPT 1 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_2_cap', 'name': 'MPPT 2 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_3_cap', 'name': 'MPPT 3 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_4_cap', 'name': 'MPPT 4 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_5_cap', 'name': 'MPPT 5 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_6_cap', 'name': 'MPPT 6 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_7_cap', 'name': 'MPPT 7 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_8_cap', 'name': 'MPPT 8 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_9_cap', 'name': 'MPPT 9 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_10_cap', 'name': 'MPPT 10 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataStateBinarySensor', 'attribute': 'run_state', 'name': 'Status'}, ] if device.type_id == PARAM_DEVICE_TYPE_ID_EMI: entities_to_create = [ {'class': 'FusionSolarRealtimeDeviceDataTemperatureSensor', 'attribute': 'temperature', 'name': 'Temperature'}, {'class': 'FusionSolarRealtimeDeviceDataTemperatureSensor', 'attribute': 'pv_temperature', 'name': 'PV temperature'}, {'class': 'FusionSolarRealtimeDeviceDataWindSpeedSensor', 'attribute': 'wind_speed', 'name': 'Wind speed'}, {'class': 'FusionSolarRealtimeDeviceDataSensor', 'attribute': 'wind_direction', 'name': 'Wind direction'}, {'class': 'FusionSolarRealtimeDeviceDataSensor', 'attribute': 'radiant_total', 'name': 'Daily irradiation'}, {'class': 'FusionSolarRealtimeDeviceDataSensor', 'attribute': 'radiant_line', 'name': 'Irradiance'}, {'class': 'FusionSolarRealtimeDeviceDataSensor', 'attribute': 'horiz_radiant_line', 'name': 'Horizontal irradiance'}, {'class': 'FusionSolarRealtimeDeviceDataSensor', 'attribute': 'horiz_radiant_total', 'name': 'Horizontal irradiation'}, {'class': 'FusionSolarRealtimeDeviceDataStateBinarySensor', 'attribute': 'run_state', 'name': 'Status'}, ] if device.type_id == PARAM_DEVICE_TYPE_ID_GRID_METER: entities_to_create = [ {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'ab_u', 'name': 'Grid AB voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'bc_u', 'name': 'Grid BC voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'ca_u', 'name': 'Grid CA voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'a_u', 'name': 'Phase A voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'b_u', 'name': 'Phase B voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'c_u', 'name': 'Phase C voltage'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'a_i', 'name': 'Phase A current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'b_i', 'name': 'Phase B current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'c_i', 'name': 'Phase C current'}, {'class': 'FusionSolarRealtimeDeviceDataPowerSensor', 'attribute': 'active_power', 'name': 'Active power'}, {'class': 'FusionSolarRealtimeDeviceDataPowerFactorSensor', 'attribute': 'power_factor', 'name': 'Power factor'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'active_cap', 'name': 'Active energy (forward active energy)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reactive_power', 'name': 'Reactive power'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_active_cap', 'name': 'Reverse active energy'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'forward_reactive_cap', 'name': 'Forward active energy'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_reactive_cap', 'name': 'Reverse reactive energy'}, {'class': 'FusionSolarRealtimeDeviceDataPowerSensor', 'attribute': 'active_power_a', 'name': 'Active power PA'}, {'class': 'FusionSolarRealtimeDeviceDataPowerSensor', 'attribute': 'active_power_b', 'name': 'Active power PB'}, {'class': 'FusionSolarRealtimeDeviceDataPowerSensor', 'attribute': 'active_power_c', 'name': 'Active power PC'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reactive_power_a', 'name': 'Reactive power QA'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reactive_power_b', 'name': 'Reactive power QB'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reactive_power_c', 'name': 'Reactive power QC'}, {'class': 'FusionSolarRealtimeDeviceDataApparentPowerSensor', 'attribute': 'total_apparent_power', 'name': 'Total apparent power'}, {'class': 'FusionSolarRealtimeDeviceDataFrequencySensor', 'attribute': 'grid_frequency', 'name': 'Grid frequency'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_active_peak', 'name': 'Reverse active energy (peak)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_active_power', 'name': 'Reverse active energy (shoulder)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_active_valley', 'name': 'Reverse active energy (off-peak)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_active_top', 'name': 'Reverse active energy (sharp)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'positive_active_peak', 'name': 'Forward active energy (peak)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'positive_active_power', 'name': 'Forward active energy (shoulder)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'positive_active_valley', 'name': 'Forward active energy (off-peak)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'positive_active_top', 'name': 'Forward active energy (sharp)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reverse_reactive_peak', 'name': 'Reverse reactive energy (peak)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reverse_reactive_power', 'name': 'Reverse reactive energy (shoulder)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reverse_reactive_valley', 'name': 'Reverse reactive energy (off-peak)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reverse_reactive_top', 'name': 'Reverse reactive energy (sharp)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'positive_reactive_peak', 'name': 'Forward reactive energy (peak)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'positive_reactive_power', 'name': 'Forward reactive energy (shoulder)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'positive_reactive_valley', 'name': 'Forward reactive energy (off-peak)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'positive_reactive_top', 'name': 'Forward reactive energy (sharp)'}, ] if device.type_id == PARAM_DEVICE_TYPE_ID_RESIDENTIAL_INVERTER: entities_to_create = [ {'class': 'FusionSolarRealtimeDeviceDataTranslatedSensor', 'attribute': 'inverter_state', 'name': 'Inverter status'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'ab_u', 'name': 'Grid AB voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'bc_u', 'name': 'Grid BC voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'ca_u', 'name': 'Grid CA voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'a_u', 'name': 'Phase A voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'b_u', 'name': 'Phase B voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'c_u', 'name': 'Phase C voltage'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'a_i', 'name': 'Phase A current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'b_i', 'name': 'Phase B current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'c_i', 'name': 'Phase C current'}, {'class': 'FusionSolarRealtimeDeviceDataPercentageSensor', 'attribute': 'efficiency', 'name': 'Inverter efficiency % (manufacturer)'}, {'class': 'FusionSolarRealtimeDeviceDataTemperatureSensor', 'attribute': 'temperature', 'name': 'Inverter internal temperature'}, {'class': 'FusionSolarRealtimeDeviceDataPowerFactorSensor', 'attribute': 'power_factor', 'name': 'Power factor'}, {'class': 'FusionSolarRealtimeDeviceDataFrequencySensor', 'attribute': 'elec_freq', 'name': 'Grid frequency'}, {'class': 'FusionSolarRealtimeDeviceDataPowerSensor', 'attribute': 'active_power', 'name': 'Active power'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reactive_power', 'name': 'Reactive output power'}, {'class': 'FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor', 'attribute': 'day_cap', 'name': 'Yield Today'}, {'class': 'FusionSolarRealtimeDeviceDataPowerSensor', 'attribute': 'mppt_power', 'name': 'MPPT total input power'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv1_u', 'name': 'PV1 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv2_u', 'name': 'PV2 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv3_u', 'name': 'PV3 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv4_u', 'name': 'PV4 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv5_u', 'name': 'PV5 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv6_u', 'name': 'PV6 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv7_u', 'name': 'PV7 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'pv8_u', 'name': 'PV8 input voltage'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv1_i', 'name': 'PV1 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv2_i', 'name': 'PV2 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv3_i', 'name': 'PV3 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv4_i', 'name': 'PV4 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv5_i', 'name': 'PV5 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv6_i', 'name': 'PV6 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv7_i', 'name': 'PV7 input current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'pv8_i', 'name': 'PV8 input current'}, {'class': 'FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor', 'attribute': 'total_cap', 'name': 'Total yield'}, {'class': 'FusionSolarRealtimeDeviceDataTimestampSensor', 'attribute': 'open_time', 'name': 'Inverter startup time'}, {'class': 'FusionSolarRealtimeDeviceDataTimestampSensor', 'attribute': 'close_time', 'name': 'Inverter shutdown time'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_1_cap', 'name': 'MPPT 1 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_2_cap', 'name': 'MPPT 2 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_3_cap', 'name': 'MPPT 3 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'mppt_4_cap', 'name': 'MPPT 4 DC total yield'}, {'class': 'FusionSolarRealtimeDeviceDataStateBinarySensor', 'attribute': 'run_state', 'name': 'Status'}, ] if device.type_id == PARAM_DEVICE_TYPE_ID_BATTERY: entities_to_create = [ {'class': 'FusionSolarRealtimeDeviceDataTranslatedSensor', 'attribute': 'battery_status', 'name': 'Battery running status'}, {'class': 'FusionSolarRealtimeDeviceDataPowerInWattSensor', 'attribute': 'max_charge_power', 'name': 'Maximum charge power'}, {'class': 'FusionSolarRealtimeDeviceDataPowerInWattSensor', 'attribute': 'max_discharge_power', 'name': 'Maximum discharge power'}, {'class': 'FusionSolarRealtimeDeviceDataPowerInWattSensor', 'attribute': 'ch_discharge_power', 'name': 'Charge/Discharge power'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'busbar_u', 'name': 'Battery voltage'}, {'class': 'FusionSolarRealtimeDeviceDataBatterySensor', 'attribute': 'battery_soc', 'name': 'Battery state of charge (SOC)'}, {'class': 'FusionSolarRealtimeDeviceDataSensor', 'attribute': 'battery_soh', 'name': 'Battery state of health (SOH)'}, {'class': 'FusionSolarRealtimeDeviceDataTranslatedSensor', 'attribute': 'ch_discharge_model', 'name': 'Charge/Discharge mode'}, {'class': 'FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor', 'attribute': 'charge_cap', 'name': 'Charging capacity'}, {'class': 'FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor', 'attribute': 'discharge_cap', 'name': 'Discharging capacity'}, {'class': 'FusionSolarRealtimeDeviceDataStateBinarySensor', 'attribute': 'run_state', 'name': 'Status'}, ] if device.type_id == PARAM_DEVICE_TYPE_ID_C_I_UTILITY_ESS: entities_to_create = [ {'class': 'FusionSolarRealtimeDeviceDataPowerInWattSensor', 'attribute': 'ch_discharge_power', 'name': 'Charge/Discharge power'}, {'class': 'FusionSolarRealtimeDeviceDataBatterySensor', 'attribute': 'battery_soc', 'name': 'Battery state of charge (SOC)'}, {'class': 'FusionSolarRealtimeDeviceDataSensor', 'attribute': 'battery_soh', 'name': 'Battery state of health (SOH)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor', 'attribute': 'charge_cap', 'name': 'Charging capacity'}, {'class': 'FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor', 'attribute': 'discharge_cap', 'name': 'Discharging capacity'}, {'class': 'FusionSolarRealtimeDeviceDataStateBinarySensor', 'attribute': 'run_state', 'name': 'Status'}, ] if device.type_id == PARAM_DEVICE_TYPE_ID_POWER_SENSOR: entities_to_create = [ {'class': 'FusionSolarRealtimeDeviceDataTranslatedSensor', 'attribute': 'meter_status', 'name': 'Meter status'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'meter_u', 'name': 'Grid voltage'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'meter_i', 'name': 'Grid current'}, {'class': 'FusionSolarRealtimeDeviceDataPowerInWattSensor', 'attribute': 'active_power', 'name': 'Active power'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerInVarSensor', 'attribute': 'reactive_power', 'name': 'Reactive power'}, {'class': 'FusionSolarRealtimeDeviceDataPowerFactorSensor', 'attribute': 'power_factor', 'name': 'Power factor'}, {'class': 'FusionSolarRealtimeDeviceDataFrequencySensor', 'attribute': 'grid_frequency', 'name': 'Grid frequency'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'active_cap', 'name': 'Active energy (forward active energy)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_active_cap', 'name': 'Reverse active energy'}, {'class': 'FusionSolarRealtimeDeviceDataStateBinarySensor', 'attribute': 'run_state', 'name': 'Status'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'ab_u', 'name': 'A-B line voltage of grid'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'bc_u', 'name': 'B-C line voltage of grid'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'ca_u', 'name': 'C-A line voltage of grid'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'a_u', 'name': 'Phase A voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'b_u', 'name': 'Phase B voltage'}, {'class': 'FusionSolarRealtimeDeviceDataVoltageSensor', 'attribute': 'c_u', 'name': 'Phase C voltage'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'a_i', 'name': 'Phase A current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'b_i', 'name': 'Phase B current'}, {'class': 'FusionSolarRealtimeDeviceDataCurrentSensor', 'attribute': 'c_i', 'name': 'Phase C current'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'forward_reactive_cap', 'name': 'Positive reactive energy'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_reactive_cap', 'name': 'Negative reactive energy'}, {'class': 'FusionSolarRealtimeDeviceDataPowerInWattSensor', 'attribute': 'active_power_a', 'name': 'Active power PA'}, {'class': 'FusionSolarRealtimeDeviceDataPowerInWattSensor', 'attribute': 'active_power_b', 'name': 'Active power PB'}, {'class': 'FusionSolarRealtimeDeviceDataPowerInWattSensor', 'attribute': 'active_power_c', 'name': 'Active power PC'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reactive_power_a', 'name': 'Reactive power QA'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reactive_power_b', 'name': 'Reactive power QB'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reactive_power_c', 'name': 'Reactive power QC'}, {'class': 'FusionSolarRealtimeDeviceDataApparentPowerSensor', 'attribute': 'total_apparent_power', 'name': 'Total apparent power'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_active_peak', 'name': 'Negative active energy (peak)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_active_power', 'name': 'Negative active energy (shoulder)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_active_valley', 'name': 'Negative active energy (off-peak)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'reverse_active_top', 'name': 'Negative active energy (sharp)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'positive_active_peak', 'name': 'Positive active energy (peak)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'positive_active_power', 'name': 'Positive active energy (shoulder)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'positive_active_valley', 'name': 'Positive active energy (off-peak)'}, {'class': 'FusionSolarRealtimeDeviceDataEnergySensor', 'attribute': 'positive_active_top', 'name': 'Positive active energy (sharp)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reverse_reactive_peak', 'name': 'Negative reactive energy (peak)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reverse_reactive_power', 'name': 'Negative reactive energy (shoulder)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reverse_reactive_valley', 'name': 'Negative reactive energy (off-peak)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'reverse_reactive_top', 'name': 'Negative reactive energy (sharp)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'positive_reactive_peak', 'name': 'Positive reactive energy (peak)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'positive_reactive_power', 'name': 'Positive reactive energy (shoulder)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'positive_reactive_valley', 'name': 'Positive reactive energy (off-peak)'}, {'class': 'FusionSolarRealtimeDeviceDataReactivePowerSensor', 'attribute': 'positive_reactive_top', 'name': 'Positive reactive energy (sharp)'}, ] entities = [] for entity_to_create in entities_to_create: class_name = globals()[entity_to_create['class']] entities.append( class_name(coordinator, device, entity_to_create['name'], entity_to_create['attribute']) ) async_add_entities(entities) async def _add_entities_for_stations_real_kpi_data(hass, async_add_entities, stations, api: FusionSolarOpenApi): device_registry = dr.async_get(hass) stations = list(filter(lambda x: filter_for_enabled_stations(x, device_registry), stations)) station_codes = [station.code for station in stations] _LOGGER.debug(f'Adding stations_real_kpi_data entities for stations ({len(station_codes)})') async def async_update_station_real_kpi_data(): """Fetch data""" data = {} if station_codes is None or len(station_codes) == 0: return data try: response = await hass.async_add_executor_job(api.get_station_real_kpi, station_codes) except FusionSolarOpenApiError as error: raise UpdateFailed(f'OpenAPI Error: {error}') for response_data in response: data[f'{DOMAIN}-{response_data[ATTR_STATION_CODE]}'] = response_data[ATTR_STATION_REAL_KPI_DATA_ITEM_MAP] return data coordinator = DataUpdateCoordinator( hass, _LOGGER, name='FusionSolarOpenAPIStationRealKpi', update_method=async_update_station_real_kpi_data, update_interval=timedelta(seconds=600), ) # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() for station in stations: entities_to_create = [ {'class': 'FusionSolarStationAttributeEntity', 'name': 'Station Code', 'suffix': 'station_code', 'value': station.code}, {'class': 'FusionSolarStationAttributeEntity', 'name': 'Station Name', 'suffix': 'station_name', 'value': station.name}, {'class': 'FusionSolarStationAddressEntity', 'name': 'Station Address', 'suffix': 'station_address', 'value': station.address}, {'class': 'FusionSolarStationCapacityEntity', 'name': 'Capacity', 'suffix': 'capacity', 'value': station.capacity}, {'class': 'FusionSolarStationContactPersonEntity', 'name': 'Contact Person', 'suffix': 'contact_person', 'value': station.contact_person}, {'class': 'FusionSolarStationContactPersonPhoneEntity', 'name': 'Contact Phone', 'suffix': 'contact_phone', 'value': station.contact_phone}, ] entities = [] for entity_to_create in entities_to_create: class_name = globals()[entity_to_create['class']] entities.append( class_name(station, entity_to_create['name'], entity_to_create['suffix'], entity_to_create['value'], ) ) async_add_entities(entities) async_add_entities([ FusionSolarEnergySensorTotalCurrentDay( coordinator, f'{DOMAIN}-{station.code}-{ID_TOTAL_CURRENT_DAY_ENERGY}', f'{station.readable_name} - {NAME_TOTAL_CURRENT_DAY_ENERGY}', ATTR_STATION_REAL_KPI_TOTAL_CURRENT_DAY_ENERGY, f'{DOMAIN}-{station.code}', station.device_info() ), FusionSolarEnergySensorTotalCurrentMonth( coordinator, f'{DOMAIN}-{station.code}-{ID_TOTAL_CURRENT_MONTH_ENERGY}', f'{station.readable_name} - {NAME_TOTAL_CURRENT_MONTH_ENERGY}', ATTR_STATION_REAL_KPI_TOTAL_CURRENT_MONTH_ENERGY, f'{DOMAIN}-{station.code}', station.device_info() ), FusionSolarEnergySensorTotalLifetime( coordinator, f'{DOMAIN}-{station.code}-{ID_TOTAL_LIFETIME_ENERGY}', f'{station.readable_name} - {NAME_TOTAL_LIFETIME_ENERGY}', ATTR_STATION_REAL_KPI_TOTAL_LIFETIME_ENERGY, f'{DOMAIN}-{station.code}', station.device_info() ) ]) async def _add_entities_for_stations_year_kpi_data(hass, async_add_entities, stations, api: FusionSolarOpenApi): device_registry = dr.async_get(hass) stations = list(filter(lambda x: filter_for_enabled_stations(x, device_registry), stations)) station_codes = [station.code for station in stations] _LOGGER.debug(f'Adding stations_year_kpi_data entities for stations ({len(station_codes)})') async def async_update_station_year_kpi_data(): data = {} if station_codes is None or len(station_codes) == 0: return data try: response = await hass.async_add_executor_job(api.get_kpi_station_year, station_codes) except FusionSolarOpenApiError as error: raise UpdateFailed(f'OpenAPI Error: {error}') for response_data in response: key = f'{DOMAIN}-{response_data[ATTR_STATION_CODE]}' if key not in data: data[key] = {} data[key][response_data[ATTR_DATA_COLLECT_TIME]] = response_data[ATTR_STATION_REAL_KPI_DATA_ITEM_MAP] _LOGGER.debug(f'async_update_station_year_kpi_data: {data}') return data coordinator = DataUpdateCoordinator( hass, _LOGGER, name='FusionSolarOpenAPIStationYearKpi', update_method=async_update_station_year_kpi_data, update_interval=timedelta(hours=1), ) # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() for station in stations: entities_to_create = [ {'class': 'FusionSolarYearPlantDataInstalledCapacitySensor'}, {'class': 'FusionSolarYearPlantDataRadiationIntensitySensor'}, {'class': 'FusionSolarYearPlantDataTheoryPowerSensor'}, {'class': 'FusionSolarYearPlantDataPerformanceRatioSensor'}, {'class': 'FusionSolarYearPlantDataInverterPowerSensor'}, {'class': 'FusionSolarBackwardsCompatibilityTotalCurrentYear'}, {'class': 'FusionSolarYearPlantDataOngridPowerSensor'}, {'class': 'FusionSolarYearPlantDataUsePowerSensor'}, {'class': 'FusionSolarYearPlantDataPowerProfitSensor'}, {'class': 'FusionSolarYearPlantDataPerpowerRatioSensor'}, {'class': 'FusionSolarYearPlantDataReductionTotalCo2Sensor'}, {'class': 'FusionSolarYearPlantDataReductionTotalCoalSensor'}, {'class': 'FusionSolarYearPlantDataReductionTotalTreeSensor'}, {'class': 'FusionSolarLifetimePlantDataInverterPowerSensor'}, {'class': 'FusionSolarLifetimePlantDataOngridPowerSensor'}, {'class': 'FusionSolarLifetimePlantDataUsePowerSensor'}, {'class': 'FusionSolarLifetimePlantDataPowerProfitSensor'}, {'class': 'FusionSolarLifetimePlantDataPerpowerRatioSensor'}, {'class': 'FusionSolarLifetimePlantDataReductionTotalCo2Sensor'}, {'class': 'FusionSolarLifetimePlantDataReductionTotalCoalSensor'}, {'class': 'FusionSolarLifetimePlantDataReductionTotalTreeSensor'}, ] entities = [] for entity_to_create in entities_to_create: class_name = globals()[entity_to_create['class']] entities.append(class_name(coordinator, station)) async_add_entities(entities) async def _add_static_entities_for_devices(async_add_entities, devices): for device in devices: entities_to_create = [ {'class': 'FusionSolarDeviceAttributeEntity', 'name': 'Device ID', 'suffix': 'device_id', 'value': device.device_id}, {'class': 'FusionSolarDeviceAttributeEntity', 'name': 'Device name', 'suffix': 'device_name', 'value': device.name}, {'class': 'FusionSolarDeviceAttributeEntity', 'name': 'Station code', 'suffix': 'station_code', 'value': device.station_code}, {'class': 'FusionSolarDeviceAttributeEntity', 'name': 'Serial number', 'suffix': 'esn_code', 'value': device.esn_code}, {'class': 'FusionSolarDeviceAttributeEntity', 'name': 'Device type ID', 'suffix': 'device_type_id', 'value': device.type_id}, {'class': 'FusionSolarDeviceAttributeEntity', 'name': 'Device type', 'suffix': 'device_type', 'value': device.device_type}, {'class': 'FusionSolarDeviceLatitudeEntity', 'name': 'Latitude', 'suffix': 'latitude', 'value': device.latitude}, {'class': 'FusionSolarDeviceLongitudeEntity', 'name': 'Longitude', 'suffix': 'longitude', 'value': device.longitude}, ] if device.type_id in [PARAM_DEVICE_TYPE_ID_STRING_INVERTER, PARAM_DEVICE_TYPE_ID_RESIDENTIAL_INVERTER]: entities_to_create.append({ 'class': 'FusionSolarDeviceAttributeEntity', 'name': 'Inverter model', 'suffix': 'inverter_type', 'value': device.inverter_type }) entities = [] for entity_to_create in entities_to_create: class_name = globals()[entity_to_create['class']] entities.append( class_name(device, entity_to_create['name'], entity_to_create['suffix'], entity_to_create['value'], ) ) async_add_entities(entities) async def async_setup_entry(hass, config_entry, async_add_entities): config = hass.data[DOMAIN][config_entry.entry_id] # Update our config to include new repos and remove those that have been removed. if config_entry.options: config.update(config_entry.options) for kioskConfig in config[CONF_KIOSKS]: kiosk = FusionSolarKiosk(kioskConfig[CONF_URL], kioskConfig[CONF_NAME]) await add_entities_for_kiosk(hass, async_add_entities, kiosk) if config[CONF_OPENAPI_CREDENTIALS]: # get stations from openapi api = FusionSolarOpenApi( config[CONF_OPENAPI_CREDENTIALS][CONF_HOST], config[CONF_OPENAPI_CREDENTIALS][CONF_USERNAME], config[CONF_OPENAPI_CREDENTIALS][CONF_PASSWORD], ) stations = await hass.async_add_executor_job(api.get_station_list) if not stations: _LOGGER.error('No stations found') raise IntegrationError('No stations found in OpenAPI') if len(stations) > 100: _LOGGER.error('More than 100 stations found, which is not a good idea.') await add_entities_for_stations(hass, async_add_entities, stations, api) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): for kioskConfig in config[CONF_KIOSKS]: kiosk = FusionSolarKiosk(kioskConfig[CONF_URL], kioskConfig[CONF_NAME]) await add_entities_for_kiosk(hass, async_add_entities, kiosk) ================================================ FILE: custom_components/fusion_solar/strings.json ================================================ { "title": "Fusion Solar", "config": { "error": { "invalid_type": "Invalid type, only kiosk or openapi are allowed.", "invalid_credentials": "Could not authenticate with the provided credentials." }, "step": { "choose_type": { "description": "FusionSolar can be connected in two ways:\n* **Kiosk**: This is the most easy way as you (probably) don't need any interaction with Huawei Fusion Solar, the drawback is the the realtime information is per 30 minutes.\n* **OpenAPI**: You will need an OpenAPI account from Huawei, this will give you better realtime information. [More information]({openapi_help_url}).", "data": { "type": "Choose the way you want to connect to Fusion Solar" } }, "kiosk": { "description": "1. Sign in on the [Huawei FusionSolar portal]({kiosk_portal_url}).\n2. Select your plant if needed.\n3. At the top there is a button: \"Kiosk\", click it.\n4. An overlay will open, and you need to enable the kiosk view by enabling the toggle.", "data": { "name": "Name", "url": "Kiosk URL", "add_another": "Add another kiosk URL" } }, "openapi": { "description": "Enter the OpenAPI credentials your received from Huawei FusionSolar below.", "data": { "host": "Host", "username": "Username", "password": "Password" } } } }, "entity": { "sensor": { "battery_status": { "state": { "0": "offline", "1": "standby", "2": "running", "3": "faulty", "4": "hibernation" } }, "ch_discharge_model": { "state": { "0": "none", "1": "forced charge/discharge", "2": "time-of-use price", "3": "fixed charge/discharge", "4": "automatic charge/discharge", "5": "fully fed to grid", "6": "TOU", "7": "remote scheduling–max. self-consumption", "8": "remote scheduling–fully fed to grid", "9": "remote scheduling–TOU", "10": "AI energy control", "11": "remote control–AI energy control", "12": "third-party dispatch" } }, "meter_status": { "state": { "0": "offline", "1": "normal" } }, "inverter_state": { "state": { "0": "Standby: initializing", "1": "Standby: insulation resistance detection", "2": "Standby: sunlight detection", "3": "Standby: power grid detection", "256": "Start", "512": "Grid connection", "513": "Grid connection: limited power", "514": "Grid connection: self-derating", "768": "Shutdown: unexpected shutdown", "769": "Shutdown: commanded shutdown", "770": "Shutdown: OVGR", "771": "Shutdown: communication disconnection", "772": "Shutdown: limited power", "773": "Shutdown: manual startup is required", "774": "Shutdown: DC switch disconnected", "1025": "Grid scheduling: cosψ-P curve", "1026": "Grid scheduling: Q-U curve", "1280": "Spot-check ready", "1281": "Spot-checking", "1536": "Inspecting", "1792": "AFCI self-check", "2048": "I-V scanning", "2304": "DC input detection", "40960": "Standby: no sunlight", "45056": "Communication disconnection (written by the SmartLogger)", "49152": "Loading (written by the SmartLogger)" } } } } } ================================================ FILE: custom_components/fusion_solar/translations/de.json ================================================ { "title": "Fusion Solar", "config": { "error": { "invalid_type": "Ung\u00fcltiger Typ, nur Kiosk oder OpenAPI sind erlaubt.", "invalid_credentials": "Die Authentifizierung mit den angegebenen Anmeldedaten war nicht m\u00f6glich." }, "step": { "choose_type": { "description": "FusionSolar kann auf zwei Arten verbunden werden:\n* **Kiosk**: Dies ist der einfachste Weg, da (wahrscheinlich) keine Interaktion mit Huawei Fusion Solar erforderlich ist, der Nachteil ist, dass die Echtzeitinformationen alle 30 Minuten aktualisiert werden.\n* **OpenAPI**: Du ben\u00f6tigst ein OpenAPI-Konto von Huawei, was dir bessere Echtzeitinformationen bietet. [Weitere Informationen]({openapi_help_url}).", "data": { "type": "W\u00e4hle die Art der Verbindung zu Fusion Solar" } }, "kiosk": { "description": "1. Melde dich im [Huawei FusionSolar-Portal]({kiosk_portal_url}) an.\n2. W\u00e4hle dein Kraftwerk, falls erforderlich.\n3. Oben gibt es einen Button: \"Kiosk\", klicke darauf.\n4. Ein Overlay wird ge\u00f6ffnet, und du musst den Kiosk-Modus aktivieren, indem du den Schalter umlegst.", "data": { "name": "Name", "url": "Kiosk-URL", "add_another": "F\u00fcge eine weitere Kiosk-URL hinzu" } }, "openapi": { "description": "Gib die OpenAPI-Anmeldedaten ein, die du von Huawei FusionSolar erhalten hast.", "data": { "host": "Host", "username": "Benutzername", "password": "Passwort" } } } }, "entity": { "sensor": { "battery_status": { "state": { "0": "offline", "1": "standby", "2": "running", "3": "fehlerhaft", "4": "Ruhezustand" } }, "ch_discharge_model": { "state": { "0": "keine", "1": "erzwungene Lade-/Entladung", "2": "Nutzungspreis", "3": "feste Lade-/Entladung", "4": "automatische Lade-/Entladung", "5": "vollst\u00e4ndige Einspeisung ins Netz", "6": "TOU", "7": "Fernsteuerung – maximaler Eigenverbrauch", "8": "Fernsteuerung – vollst\u00e4ndige Einspeisung ins Netz", "9": "Fernsteuerung – TOU", "10": "KI-Energiesteuerung", "11": "Fernsteuerung – KI-Energiesteuerung", "12": "Steuerung durch Dritte" } }, "meter_status": { "state": { "0": "offline", "1": "normal" } }, "inverter_state": { "state": { "0": "Standby: Initialisierung", "1": "Standby: Isolationswiderstandserkennung", "2": "Standby: Sonnenerkennung", "3": "Standby: Netzwerkerkennung", "256": "Start", "512": "Netzanschluss", "513": "Netzanschluss: begrenzte Leistung", "514": "Netzanschluss: Selbstabstufung", "768": "Abschaltung: unerwartete Abschaltung", "769": "Abschaltung: befohlenes Abschalten", "770": "Abschaltung: OVGR", "771": "Abschaltung: Kommunikationsunterbrechung", "772": "Abschaltung: begrenzte Leistung", "773": "Abschaltung: manuelle Inbetriebnahme erforderlich", "774": "Abschaltung: DC-Schalter getrennt", "1025": "Netzplanung: cos\u03c8-P-Kurve", "1026": "Netzplanung: Q-U-Kurve", "1280": "Spot-Check bereit", "1281": "Spot-Check l\u00e4uft", "1536": "\u00dcberpr\u00fcfung", "1792": "AFCI Selbstpr\u00fcfung", "2048": "I-V-Scanning", "2304": "DC-Eingangserkennung", "40960": "Standby: kein Sonnenlicht", "45056": "Kommunikationsunterbrechung (geschrieben vom SmartLogger)", "49152": "Laden (geschrieben vom SmartLogger)" } } } } } ================================================ FILE: custom_components/fusion_solar/translations/en.json ================================================ { "title": "Fusion Solar", "config": { "error": { "invalid_type": "Invalid type, only kiosk or openapi are allowed.", "invalid_credentials": "Could not authenticate with the provided credentials." }, "step": { "choose_type": { "description": "FusionSolar can be connected in two ways:\n* **Kiosk**: This is the most easy way as you (probably) don't need any interaction with Huawei Fusion Solar, the drawback is the the realtime information is per 30 minutes.\n* **OpenAPI**: You will need an OpenAPI account from Huawei, this will give you better realtime information. [More information]({openapi_help_url}).", "data": { "type": "Choose the way you want to connect to Fusion Solar" } }, "kiosk": { "description": "1. Sign in on the [Huawei FusionSolar portal]({kiosk_portal_url}).\n2. Select your plant if needed.\n3. At the top there is a button: \"Kiosk\", click it.\n4. An overlay will open, and you need to enable the kiosk view by enabling the toggle.", "data": { "name": "Name", "url": "Kiosk URL", "add_another": "Add another kiosk URL" } }, "openapi": { "description": "Enter the OpenAPI credentials your received from Huawei FusionSolar below.", "data": { "host": "Host", "username": "Username", "password": "Password" } } } }, "entity": { "sensor": { "battery_status": { "state": { "0": "offline", "1": "standby", "2": "running", "3": "faulty", "4": "hibernation" } }, "ch_discharge_model": { "state": { "0": "none", "1": "forced charge/discharge", "2": "time-of-use price", "3": "fixed charge/discharge", "4": "automatic charge/discharge", "5": "fully fed to grid", "6": "TOU", "7": "remote scheduling–max. self-consumption", "8": "remote scheduling–fully fed to grid", "9": "remote scheduling–TOU", "10": "AI energy control", "11": "remote control–AI energy control", "12": "third-party dispatch" } }, "meter_status": { "state": { "0": "offline", "1": "normal" } }, "inverter_state": { "state": { "0": "Standby: initializing", "1": "Standby: insulation resistance detection", "2": "Standby: sunlight detection", "3": "Standby: power grid detection", "256": "Start", "512": "Grid connection", "513": "Grid connection: limited power", "514": "Grid connection: self-derating", "768": "Shutdown: unexpected shutdown", "769": "Shutdown: commanded shutdown", "770": "Shutdown: OVGR", "771": "Shutdown: communication disconnection", "772": "Shutdown: limited power", "773": "Shutdown: manual startup is required", "774": "Shutdown: DC switch disconnected", "1025": "Grid scheduling: cosψ-P curve", "1026": "Grid scheduling: Q-U curve", "1280": "Spot-check ready", "1281": "Spot-checking", "1536": "Inspecting", "1792": "AFCI self-check", "2048": "I-V scanning", "2304": "DC input detection", "40960": "Standby: no sunlight", "45056": "Communication disconnection (written by the SmartLogger)", "49152": "Loading (written by the SmartLogger)" } } } } } ================================================ FILE: custom_components/fusion_solar/translations/es.json ================================================ { "title": "Fusion Solar", "config": { "error": { "invalid_type": "Tipo inv\u00e1lido, solo se permiten kiosk o openapi.", "invalid_credentials": "No se pudo autenticar con las credenciales proporcionadas." }, "step": { "choose_type": { "description": "FusionSolar se puede conectar de dos maneras:\n* **Kiosk**: Esta es la forma m\u00e1s f\u00e1cil, ya que (probablemente) no necesites ninguna interacci\u00f3n con Huawei Fusion Solar. Sin embargo, la informaci\u00f3n en tiempo real se actualiza cada 30 minutos.\n* **OpenAPI**: Necesitar\u00e1s una cuenta OpenAPI de Huawei, lo que te proporcionar\u00e1 una mejor informaci\u00f3n en tiempo real. [M\u00e1s informaci\u00f3n]({openapi_help_url}).", "data": { "type": "Elige la forma en la que deseas conectarte a Fusion Solar" } }, "kiosk": { "description": "1. Inicia sesi\u00f3n en el [portal Huawei FusionSolar]({kiosk_portal_url}).\n2. Selecciona tu planta si es necesario.\n3. En la parte superior hay un bot\u00f3n: \"Kiosk\", haz clic en \u00e9l.\n4. Se abrir\u00e1 una ventana emergente y deber\u00e1s habilitar la vista kiosk activando el interruptor.", "data": { "name": "Nombre", "url": "URL del Kiosk", "add_another": "Agregar otra URL de Kiosk" } }, "openapi": { "description": "Introduce las credenciales de OpenAPI que recibiste de Huawei FusionSolar a continuaci\u00f3n.", "data": { "host": "Host", "username": "Usuario", "password": "Contrase\u00f1a" } } } }, "entity": { "sensor": { "battery_status": { "state": { "0": "fuera de l\u00ednea", "1": "en espera", "2": "en funcionamiento", "3": "con falla", "4": "hibernaci\u00f3n" } }, "ch_discharge_model": { "state": { "0": "ninguno", "1": "carga/descarga forzada", "2": "precio seg\u00fan horario de uso", "3": "carga/descarga fija", "4": "carga/descarga autom\u00e1tica", "5": "inyecci\u00f3n total a la red", "6": "TOU", "7": "programaci\u00f3n remota \u2013 autoconsumo m\u00e1ximo", "8": "programaci\u00f3n remota \u2013 inyecci\u00f3n total a la red", "9": "programaci\u00f3n remota \u2013 TOU", "10": "control energ\u00e9tico mediante IA", "11": "control remoto \u2013 control energ\u00e9tico mediante IA", "12": "gesti\u00f3n por terceros" } }, "meter_status": { "state": { "0": "fuera de l\u00ednea", "1": "normal" } }, "inverter_state": { "state": { "0": "En espera: inicializando", "1": "En espera: detecci\u00f3n de resistencia de aislamiento", "2": "En espera: detecci\u00f3n de luz solar", "3": "En espera: detecci\u00f3n de la red el\u00e9ctrica", "256": "Inicio", "512": "Conexi\u00f3n a la red", "513": "Conexi\u00f3n a la red: potencia limitada", "514": "Conexi\u00f3n a la red: autolimitaci\u00f3n", "768": "Apagado: apagado inesperado", "769": "Apagado: apagado por comando", "770": "Apagado: OVGR", "771": "Apagado: desconexi\u00f3n de comunicaci\u00f3n", "772": "Apagado: potencia limitada", "773": "Apagado: se requiere inicio manual", "774": "Apagado: interruptor de CC desconectado", "1025": "Programaci\u00f3n de red: curva cos\u03c8-P", "1026": "Programaci\u00f3n de red: curva Q-U", "1280": "Listo para verificaci\u00f3n puntual", "1281": "Verificaci\u00f3n puntual en curso", "1536": "Inspeccionando", "1792": "Autocomprobaci\u00f3n AFCI", "2048": "Escaneo I-V", "2304": "Detecci\u00f3n de entrada de CC", "40960": "En espera: sin luz solar", "45056": "Desconexi\u00f3n de comunicaci\u00f3n (escrito por el SmartLogger)", "49152": "Cargando (escrito por el SmartLogger)" } } } } } ================================================ FILE: custom_components/fusion_solar/translations/fr.json ================================================ { "title": "Fusion Solar", "config": { "error": { "invalid_type": "Type invalide, seuls kiosk ou openapi sont autoris\u00e9s.", "invalid_credentials": "Impossible de s'authentifier avec les identifiants fournis." }, "step": { "choose_type": { "description": "FusionSolar peut \u00eatre connect\u00e9 de deux mani\u00e8res :\n* **Kiosk** : C'est la mani\u00e8re la plus simple, car vous n'avez (probablement) pas besoin d'interagir avec Huawei Fusion Solar. L'inconv\u00e9nient est que les informations en temps r\u00e9el sont mises \u00e0 jour toutes les 30 minutes.\n* **OpenAPI** : Vous aurez besoin d'un compte OpenAPI de Huawei, ce qui vous permettra d'obtenir des informations en temps r\u00e9el plus pr\u00e9cises. [Plus d'informations]({openapi_help_url}).", "data": { "type": "Choisissez la m\u00e9thode de connexion \u00e0 Fusion Solar" } }, "kiosk": { "description": "1. Connectez-vous au [portail Huawei FusionSolar]({kiosk_portal_url}).\n2. S\u00e9lectionnez votre site si n\u00e9cessaire.\n3. En haut, il y a un bouton : \"Kiosk\", cliquez dessus.\n4. Une fen\u00eatre s'ouvrira, et vous devrez activer l'affichage Kiosk en activant le bouton de bascule.", "data": { "name": "Nom", "url": "URL du Kiosk", "add_another": "Ajouter une autre URL de Kiosk" } }, "openapi": { "description": "Entrez les identifiants OpenAPI que vous avez re\u00e7us de Huawei FusionSolar ci-dessous.", "data": { "host": "H\u00f4te", "username": "Nom d'utilisateur", "password": "Mot de passe" } } } }, "entity": { "sensor": { "battery_status": { "state": { "0": "hors ligne", "1": "en veille", "2": "en fonctionnement", "3": "d\u00e9fectueux", "4": "hibernation" } }, "ch_discharge_model": { "state": { "0": "aucun", "1": "charge/d\u00e9charge forc\u00e9e", "2": "tarification selon l'heure", "3": "charge/d\u00e9charge fixe", "4": "charge/d\u00e9charge automatique", "5": "injection totale dans le r\u00e9seau", "6": "TOU", "7": "programmation \u00e0 distance – autoconsommation maximale", "8": "programmation \u00e0 distance – injection totale dans le réseau", "9": "programmation \u00e0 distance – TOU", "10": "contr\u00f4le \u00e9nerg\u00e9tique par IA", "11": "contr\u00f4le \u00e0 distance – contr\u00f4le \u00e9nerg\u00e9tique par IA", "12": "r\u00e9partition par un tiers" } }, "meter_status": { "state": { "0": "hors ligne", "1": "normal" } }, "inverter_state": { "state": { "0": "En veille : initialisation", "1": "En veille : d\u00e9tection de r\u00e9sistance d'isolement", "2": "En veille : d\u00e9tection de la lumi\u00e8re solaire", "3": "En veille : d\u00e9tection du r\u00e9seau \u00e9lectrique", "256": "D\u00e9marrage", "512": "Connexion au r\u00e9seau", "513": "Connexion au r\u00e9seau : puissance limit\u00e9e", "514": "Connexion au r\u00e9seau : auto-r\u00e9duction", "768": "Arr\u00eat : arr\u00eat inattendu", "769": "Arr\u00eat : arr\u00eat command\u00e9", "770": "Arr\u00eat : OVGR", "771": "Arr\u00eat : d\u00e9connexion de communication", "772": "Arr\u00eat : puissance limit\u00e9e", "773": "Arr\u00eat : red\u00e9marrage manuel requis", "774": "Arr\u00eat : interrupteur DC d\u00e9connect\u00e9", "1025": "R\u00e9gulation du r\u00e9seau : courbe cos\u03c8-P", "1026": "R\u00e9gulation du r\u00e9seau : courbe Q-U", "1280": "Pr\u00eat pour le contr\u00f4le ponctuel", "1281": "Contr\u00f4le ponctuel en cours", "1536": "Inspection en cours", "1792": "Auto-v\u00e9rification AFCI", "2048": "Balayage I-V", "2304": "D\u00e9tection d'entr\u00e9e DC", "40960": "En veille : absence de lumi\u00e8re solaire", "45056": "D\u00e9connexion de communication (\u00e9crit par le SmartLogger)", "49152": "Chargement (\u00e9crit par le SmartLogger)" } } } } } ================================================ FILE: custom_components/fusion_solar/translations/it.json ================================================ { "title": "Fusion Solar", "config": { "error": { "invalid_type": "Tipo non valido, sono consentiti solo kiosk o openapi.", "invalid_credentials": "Impossibile autenticarsi con le credenziali fornite." }, "step": { "choose_type": { "description": "FusionSolar pu\u00f2 essere connesso in due modi:\n* **Kiosk**: Questo \u00e8 il modo pi\u00f9 semplice, poich\u00e9 (probabilmente) non \u00e8 necessario alcun intervento con Huawei Fusion Solar, lo svantaggio \u00e8 che le informazioni in tempo reale sono aggiornate ogni 30 minuti.\n* **OpenAPI**: Avrai bisogno di un account OpenAPI di Huawei, che ti fornir\u00e0 migliori informazioni in tempo reale. [Maggiori informazioni]({openapi_help_url}).", "data": { "type": "Scegli il modo in cui vuoi connetterti a Fusion Solar" } }, "kiosk": { "description": "1. Accedi al [portale Huawei FusionSolar]({kiosk_portal_url}).\n2. Se necessario, seleziona il tuo impianto.\n3. In alto c'\u00e8 un pulsante: \"Kiosk\", cliccalo.\n4. Verr\u00e0 aperto un sovrapposizione e dovrai abilitare la visualizzazione del kiosk attivando l'interruttore.", "data": { "name": "Nome", "url": "URL del Kiosk", "add_another": "Aggiungi un altro URL del Kiosk" } }, "openapi": { "description": "Inserisci le credenziali OpenAPI che hai ricevuto da Huawei FusionSolar qui sotto.", "data": { "host": "Host", "username": "Nome utente", "password": "Password" } } } }, "entity": { "sensor": { "battery_status": { "state": { "0": "offline", "1": "standby", "2": "in esecuzione", "3": "guasto", "4": "ibernazione" } }, "ch_discharge_model": { "state": { "0": "nessuno", "1": "carica/scarica forzata", "2": "prezzo a tempo di utilizzo", "3": "carica/scarica fissa", "4": "carica/scarica automatica", "5": "immissione totale in rete", "6": "TOU", "7": "programmazione remota - massimo autoconsumo", "8": "programmazione remota - immissione totale in rete", "9": "programmazione remota - TOU", "10": "controllo energetico AI", "11": "controllo remoto - controllo energetico AI", "12": "dispacciamento di terze parti" } }, "meter_status": { "state": { "0": "offline", "1": "normale" } }, "inverter_state": { "state": { "0": "Standby: inizializzazione", "1": "Standby: rilevamento resistenza di isolamento", "2": "Standby: rilevamento luce solare", "3": "Standby: rilevamento rete elettrica", "256": "Avvio", "512": "Connessione alla rete", "513": "Connessione alla rete: potenza limitata", "514": "Connessione alla rete: auto-riduzione", "768": "Arresto: arresto imprevisto", "769": "Arresto: arresto comandato", "770": "Arresto: OVGR", "771": "Arresto: disconnessione della comunicazione", "772": "Arresto: potenza limitata", "773": "Arresto: \u00e8 necessaria l'accensione manuale", "774": "Arresto: interruttore DC disconnesso", "1025": "Pianificazione della rete: curva cos\u03c8-P", "1026": "Pianificazione della rete: curva Q-U", "1280": "Verifica rapida pronta", "1281": "Verifica in corso", "1536": "Ispezione in corso", "1792": "Auto-verifica AFCI", "2048": "Scansione I-V", "2304": "Rilevamento ingresso DC", "40960": "Standby: nessuna luce solare", "45056": "Disconnessione della comunicazione (scritta dal SmartLogger)", "49152": "Caricamento (scritto dal SmartLogger)" } } } } } ================================================ FILE: custom_components/fusion_solar/translations/pt.json ================================================ { "title": "Fusion Solar", "config": { "error": { "invalid_type": "Tipo inv\u00e1lido, apenas kiosk ou openapi s\u00e3o permitidos.", "invalid_credentials": "N\u00e3o foi poss\u00edvel autenticar com as credenciais fornecidas." }, "step": { "choose_type": { "description": "O FusionSolar pode ser conectado de duas maneiras:\n* **Kiosk**: Esta \u00e9 a forma mais f\u00e1cil, pois (provavelmente) voc\u00ea n\u00e3o precisar\u00e1 de intera\u00e7\u00e3o com o Huawei Fusion Solar, a desvantagem \u00e9 que as informa\u00e7\u00f5es em tempo real s\u00e3o atualizadas a cada 30 minutos.\n* **OpenAPI**: Voc\u00ea precisar\u00e1 de uma conta OpenAPI da Huawei, o que lhe dar\u00e1 melhores informa\u00e7\u00f5es em tempo real. [Mais informa\u00e7\u00f5es]({openapi_help_url}).", "data": { "type": "Escolha a forma como deseja se conectar ao Fusion Solar" } }, "kiosk": { "description": "1. Fa\u00e7a login no [portal Huawei FusionSolar]({kiosk_portal_url}).\n2. Se necess\u00e1rio, selecione sua planta.\n3. No topo, h\u00e1 um bot\u00e3o: \"Kiosk\", clique nele.\n4. Uma sobreposi\u00e7\u00e3o ser\u00e1 aberta e voc\u00ea precisar\u00e1 habilitar a visualiza\u00e7\u00e3o do kiosk ativando o alternador.", "data": { "name": "Nome", "url": "URL do Kiosk", "add_another": "Adicionar outra URL de Kiosk" } }, "openapi": { "description": "Digite as credenciais OpenAPI que voc\u00ea recebeu do Huawei FusionSolar abaixo.", "data": { "host": "Host", "username": "Nome de usu\u00e1rio", "password": "Senha" } } } }, "entity": { "sensor": { "battery_status": { "state": { "0": "offline", "1": "standby", "2": "running", "3": "faulty", "4": "hibernation" } }, "ch_discharge_model": { "state": { "0": "nenhum", "1": "carga/descarga for\u00e7ada", "2": "pre\u00e7o de uso hor\u00e1rio", "3": "carga/descarga fixa", "4": "carga/descarga autom\u00e1tica", "5": "alimentação total para a rede", "6": "TOU", "7": "programação remota - autoconsumo m\u00e1ximo", "8": "programação remota - alimenta\u00e7\u00e3o total para a rede", "9": "programação remota - TOU", "10": "controlo de energia por IA", "11": "controlo remoto - controlo de energia por IA", "12": "despacho por terceiros" } }, "meter_status": { "state": { "0": "offline", "1": "normal" } }, "inverter_state": { "state": { "0": "Standby: inicializando", "1": "Standby: detec\u00e7\u00e3o de resist\u00eancia de isolamento", "2": "Standby: detec\u00e7\u00e3o de luz solar", "3": "Standby: detec\u00e7\u00e3o de rede el\u00e9trica", "256": "Iniciar", "512": "Conex\u00e3o com a rede", "513": "Conex\u00e3o com a rede: pot\u00eancia limitada", "514": "Conex\u00e3o com a rede: auto-redu\u00e7\u00e3o", "768": "Desligamento: desligamento inesperado", "769": "Desligamento: desligamento comandado", "770": "Desligamento: OVGR", "771": "Desligamento: desconex\u00e3o de comunica\u00e7\u00e3o", "772": "Desligamento: pot\u00eancia limitada", "773": "Desligamento: inicializa\u00e7\u00e3o manual necess\u00e1ria", "774": "Desligamento: interruptor DC desconectado", "1025": "Planejamento da rede: curva cos\u03c8-P", "1026": "Planejamento da rede: curva Q-U", "1280": "Verifica\u00e7\u00e3o r\u00e1pida pronta", "1281": "Verificando", "1536": "Inspecionando", "1792": "Auto-check AFCI", "2048": "Escaneamento I-V", "2304": "Detec\u00e7\u00e3o de entrada DC", "40960": "Standby: sem luz solar", "45056": "Desconex\u00e3o de comunica\u00e7\u00e3o (escrito pelo SmartLogger)", "49152": "Carregando (escrito pelo SmartLogger)" } } } } } ================================================ FILE: docs/postman_collection.json ================================================ { "info": { "_postman_id": "34276b21-5c16-49da-9369-23e28183ee63", "name": "FusionSolar", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "6404377" }, "item": [ { "name": "thirdData/login", "event": [ { "listen": "test", "script": { "exec": [ "postman.setEnvironmentVariable('XSRF-TOKEN', responseHeaders[\"xsrf-token\"])" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [], "body": { "mode": "raw", "raw": "{\n \"userName\": \"{{USERNAME}}\",\n \"systemCode\": \"{{SYSTEMCODE}}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/login", "host": [ "{{URL}}" ], "path": [ "thirdData", "login" ] } }, "response": [] }, { "name": "thirdData/logout", "event": [ { "listen": "test", "script": { "exec": [ "postman.setEnvironmentVariable('XSRF-TOKEN', responseHeaders[\"xsrf-token\"])" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [], "body": { "mode": "raw", "raw": "{ \n \"xsrfToken\": \"{{XSRF-TOKEN}}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/logout", "host": [ "{{URL}}" ], "path": [ "thirdData", "logout" ] } }, "response": [] }, { "name": "thirdData/getStationList", "event": [ { "listen": "test", "script": { "exec": [ "const jsonData = JSON.parse(responseBody);", "postman.setEnvironmentVariable('stationCode', jsonData.data[0].stationCode);" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getStationList", "host": [ "{{URL}}" ], "path": [ "thirdData", "getStationList" ] } }, "response": [] }, { "name": "thirdData/stations", "event": [ { "listen": "test", "script": { "exec": [ "const jsonData = JSON.parse(responseBody);", "postman.setEnvironmentVariable('stationCode', jsonData.data.list[0].plantCode);", "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"pageNo\": 1,\n \"pageSize\": 100\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/stations", "host": [ "{{URL}}" ], "path": [ "thirdData", "stations" ] } }, "response": [] }, { "name": "thirdData/getStationRealKpi", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"stationCodes\": \"{{stationCode}}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getStationRealKpi", "host": [ "{{URL}}" ], "path": [ "thirdData", "getStationRealKpi" ] } }, "response": [] }, { "name": "thirdData/getKpiStationHour", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"stationCodes\": \"{{stationCode}}\",\n \"collectTime\": {{$timestamp}}\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getKpiStationHour", "host": [ "{{URL}}" ], "path": [ "thirdData", "getKpiStationHour" ] } }, "response": [] }, { "name": "thirdData/getKpiStationDay", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"stationCodes\": \"{{stationCode}}\",\n \"collectTime\": 1501862400000\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getKpiStationDay", "host": [ "{{URL}}" ], "path": [ "thirdData", "getKpiStationDay" ] } }, "response": [] }, { "name": "thirdData/getKpiStationMonth", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"stationCodes\": \"{{stationCode}}\",\n \"collectTime\": 1483200000000\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getKpiStationMonth", "host": [ "{{URL}}" ], "path": [ "thirdData", "getKpiStationMonth" ] } }, "response": [] }, { "name": "thirdData/getKpiStationYear", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"stationCodes\": \"{{stationCode}}\",\n \"collectTime\": 1666617789319\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getKpiStationYear", "host": [ "{{URL}}" ], "path": [ "thirdData", "getKpiStationYear" ] } }, "response": [] }, { "name": "thirdData/getAlarmList", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"stationCodes\": \"{{stationCode}}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getAlarmList", "host": [ "{{URL}}" ], "path": [ "thirdData", "getAlarmList" ] } }, "response": [] }, { "name": "thirdData/getDevList", "event": [ { "listen": "test", "script": { "exec": [ "const jsonData = JSON.parse(responseBody);", "console.log(jsonData)", "postman.setEnvironmentVariable('devId', jsonData.data[0].id);", "postman.setEnvironmentVariable('devTypeId', jsonData.data[0].devTypeId);" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"stationCodes\": \"{{stationCode}}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getDevList", "host": [ "{{URL}}" ], "path": [ "thirdData", "getDevList" ] } }, "response": [] }, { "name": "thirdData/getDevRealKpi", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"devIds\": \"{{devId}}\",\n \"devTypeId\": \"{{devTypeId}}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getDevRealKpi", "host": [ "{{URL}}" ], "path": [ "thirdData", "getDevRealKpi" ] } }, "response": [] }, { "name": "thirdData/getDevFiveMinutes", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"devIds\": \"{{devId}}\",\n \"devTypeId\": \"{{devTypeId}}\",\n \"collectTime\": 1501862400000\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getDevFiveMinutes", "host": [ "{{URL}}" ], "path": [ "thirdData", "getDevFiveMinutes" ] } }, "response": [] }, { "name": "thirdData/getDevKpiDay", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"devIds\": \"{{devId}}\",\n \"devTypeId\": \"{{devTypeId}}\",\n \"collectTime\": 1501862400000\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getDevKpiDay", "host": [ "{{URL}}" ], "path": [ "thirdData", "getDevKpiDay" ] } }, "response": [] }, { "name": "thirdData/getDevKpiMonth", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"devIds\": \"{{devId}}\",\n \"devTypeId\": \"{{devTypeId}}\",\n \"collectTime\": 1501862400000\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getDevKpiMonth", "host": [ "{{URL}}" ], "path": [ "thirdData", "getDevKpiMonth" ] } }, "response": [] }, { "name": "thirdData/getDevKpiYear", "event": [ { "listen": "test", "script": { "exec": [ "" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "strictSSL": false }, "request": { "method": "POST", "header": [ { "key": "XSRF-TOKEN", "value": "{{XSRF-TOKEN}}", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"devIds\": \"{{devId}}\",\n \"devTypeId\": \"{{devTypeId}}\",\n \"collectTime\": 1501862400000\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{URL}}/thirdData/getDevKpiYear", "host": [ "{{URL}}" ], "path": [ "thirdData", "getDevKpiYear" ] } }, "response": [] } ] } ================================================ FILE: hacs.json ================================================ { "name": "Fusion Solar", "render_readme": true }