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
[](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
}
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
SYMBOL INDEX (291 symbols across 16 files)
FILE: custom_components/fusion_solar/__init__.py
function async_setup (line 16) | async def async_setup(hass: HomeAssistant, config: Config) -> bool:
function async_setup_entry (line 22) | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> ...
function async_unload_entry (line 33) | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) ->...
FILE: custom_components/fusion_solar/config_flow.py
class FusionSolarConfigFlow (line 15) | class FusionSolarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
method async_step_user (line 21) | async def async_step_user(self, user_input: Optional[Dict[str, Any]] =...
method async_step_choose_type (line 25) | async def async_step_choose_type(self, user_input: Optional[Dict[str, ...
method async_step_kiosk (line 52) | async def async_step_kiosk(self, user_input: Optional[Dict[str, Any]] ...
method async_step_openapi (line 82) | async def async_step_openapi(self, user_input: Optional[Dict[str, Any]...
FILE: custom_components/fusion_solar/device_real_kpi_coordinator.py
class DeviceRealKpiDataCoordinator (line 20) | class DeviceRealKpiDataCoordinator(DataUpdateCoordinator):
method __init__ (line 21) | def __init__(self, hass, api, devices):
method _async_update_data (line 37) | async def _async_update_data(self):
method device_ids_grouped_per_type_id (line 81) | def device_ids_grouped_per_type_id(self):
method counter_limit (line 111) | def counter_limit(self) -> int:
method should_skip (line 115) | def should_skip(self) -> bool:
method skip_for (line 119) | def skip_for(self) -> int:
FILE: custom_components/fusion_solar/fusion_solar/device_attribute_entity.py
class FusionSolarDeviceAttributeEntity (line 7) | class FusionSolarDeviceAttributeEntity(Entity):
method __init__ (line 8) | def __init__(
method unique_id (line 23) | def unique_id(self) -> str:
method name (line 27) | def name(self):
method state (line 31) | def state(self):
method device_info (line 35) | def device_info(self) -> dict:
method entity_category (line 39) | def entity_category(self) -> str:
method should_poll (line 43) | def should_poll(self) -> bool:
class FusionSolarDeviceLatitudeEntity (line 47) | class FusionSolarDeviceLatitudeEntity(FusionSolarDeviceAttributeEntity):
class FusionSolarDeviceLongitudeEntity (line 51) | class FusionSolarDeviceLongitudeEntity(FusionSolarDeviceAttributeEntity):
FILE: custom_components/fusion_solar/fusion_solar/energy_sensor.py
class FusionSolarEnergySensor (line 13) | class FusionSolarEnergySensor(CoordinatorEntity, SensorEntity):
method __init__ (line 16) | def __init__(
method device_class (line 34) | def device_class(self) -> str:
method unique_id (line 38) | def unique_id(self) -> str:
method name (line 42) | def name(self) -> str:
method native_value (line 46) | def native_value(self) -> float:
method native_unit_of_measurement (line 71) | def native_unit_of_measurement(self) -> str:
method state_class (line 75) | def state_class(self) -> str:
method device_info (line 79) | def device_info(self) -> dict:
method is_producing_at_the_moment (line 82) | def is_producing_at_the_moment(self) -> bool:
method get_float_value_from_coordinator (line 90) | def get_float_value_from_coordinator(self, attribute_name: str) -> float:
class FusionSolarEnergySensorTotalCurrentDay (line 110) | class FusionSolarEnergySensorTotalCurrentDay(FusionSolarEnergySensor):
class FusionSolarEnergySensorTotalCurrentMonth (line 114) | class FusionSolarEnergySensorTotalCurrentMonth(FusionSolarEnergySensor):
class FusionSolarEnergySensorTotalCurrentYear (line 118) | class FusionSolarEnergySensorTotalCurrentYear(FusionSolarEnergySensor):
class FusionSolarEnergySensorTotalLifetime (line 122) | class FusionSolarEnergySensorTotalLifetime(FusionSolarEnergySensor):
class FusionSolarEnergySensorException (line 126) | class FusionSolarEnergySensorException(Exception):
FILE: custom_components/fusion_solar/fusion_solar/kiosk/kiosk.py
class FusionSolarKiosk (line 9) | class FusionSolarKiosk:
method __init__ (line 10) | def __init__(self, url, name):
method _parseId (line 15) | def _parseId(self):
method apiUrl (line 20) | def apiUrl(self):
FILE: custom_components/fusion_solar/fusion_solar/kiosk/kiosk_api.py
class FusionSolarKioskApi (line 18) | class FusionSolarKioskApi:
method __init__ (line 19) | def __init__(self, host):
method getRealTimeKpi (line 22) | def getRealTimeKpi(self, id: str):
class FusionSolarKioskApiError (line 54) | class FusionSolarKioskApiError(Exception):
FILE: custom_components/fusion_solar/fusion_solar/lifetime_plant_data_entity.py
class FusionSolarLifetimePlantDataSensor (line 12) | class FusionSolarLifetimePlantDataSensor(CoordinatorEntity, SensorEntity):
method __init__ (line 15) | def __init__(
method unique_id (line 26) | def unique_id(self) -> str:
method native_value (line 30) | def native_value(self) -> float:
method device_info (line 47) | def device_info(self) -> dict:
class FusionSolarLifetimePlantDataInverterPowerSensor (line 51) | class FusionSolarLifetimePlantDataInverterPowerSensor(FusionSolarLifetim...
method name (line 55) | def name(self) -> str:
method device_class (line 59) | def device_class(self) -> str:
method native_unit_of_measurement (line 63) | def native_unit_of_measurement(self) -> str:
method state_class (line 67) | def state_class(self) -> str:
class FusionSolarLifetimePlantDataOngridPowerSensor (line 71) | class FusionSolarLifetimePlantDataOngridPowerSensor(FusionSolarLifetimeP...
method name (line 75) | def name(self) -> str:
method device_class (line 79) | def device_class(self) -> str:
method native_unit_of_measurement (line 83) | def native_unit_of_measurement(self) -> str:
method state_class (line 87) | def state_class(self) -> str:
class FusionSolarLifetimePlantDataUsePowerSensor (line 91) | class FusionSolarLifetimePlantDataUsePowerSensor(FusionSolarLifetimePlan...
method name (line 95) | def name(self) -> str:
method device_class (line 99) | def device_class(self) -> str:
method native_unit_of_measurement (line 103) | def native_unit_of_measurement(self) -> str:
method state_class (line 107) | def state_class(self) -> str:
class FusionSolarLifetimePlantDataPowerProfitSensor (line 111) | class FusionSolarLifetimePlantDataPowerProfitSensor(FusionSolarLifetimeP...
method name (line 115) | def name(self) -> str:
method state_class (line 119) | def state_class(self) -> str:
method icon (line 123) | def icon(self) -> str | None:
class FusionSolarLifetimePlantDataPerpowerRatioSensor (line 127) | class FusionSolarLifetimePlantDataPerpowerRatioSensor(FusionSolarLifetim...
method name (line 131) | def name(self) -> str:
method state_class (line 135) | def state_class(self) -> str:
class FusionSolarLifetimePlantDataReductionTotalCo2Sensor (line 139) | class FusionSolarLifetimePlantDataReductionTotalCo2Sensor(FusionSolarLif...
method name (line 143) | def name(self) -> str:
method native_unit_of_measurement (line 147) | def native_unit_of_measurement(self) -> str:
method state_class (line 151) | def state_class(self) -> str:
method native_value (line 155) | def native_value(self) -> float:
method icon (line 164) | def icon(self) -> str | None:
class FusionSolarLifetimePlantDataReductionTotalCoalSensor (line 168) | class FusionSolarLifetimePlantDataReductionTotalCoalSensor(FusionSolarLi...
method name (line 172) | def name(self) -> str:
method native_unit_of_measurement (line 176) | def native_unit_of_measurement(self) -> str:
method state_class (line 180) | def state_class(self) -> str:
method native_value (line 184) | def native_value(self) -> float:
method icon (line 193) | def icon(self) -> str | None:
class FusionSolarLifetimePlantDataReductionTotalTreeSensor (line 197) | class FusionSolarLifetimePlantDataReductionTotalTreeSensor(FusionSolarLi...
method name (line 201) | def name(self) -> str:
method state_class (line 205) | def state_class(self) -> str:
method icon (line 209) | def icon(self) -> str | None:
FILE: custom_components/fusion_solar/fusion_solar/openapi/device.py
class FusionSolarDevice (line 4) | class FusionSolarDevice:
method __init__ (line 5) | def __init__(
method model (line 28) | def model(self) -> str:
method device_type (line 35) | def device_type(self) -> str:
method device_info (line 77) | def device_info(self):
method readable_name (line 90) | def readable_name(self):
FILE: custom_components/fusion_solar/fusion_solar/openapi/openapi_api.py
class FusionSolarOpenApi (line 22) | class FusionSolarOpenApi:
method __init__ (line 23) | def __init__(self, host: str, username: str, password: str):
method login (line 29) | def login(self) -> str:
method get_station_list (line 51) | def get_station_list(self):
method stations (line 78) | def stations(self):
method get_station_real_kpi (line 103) | def get_station_real_kpi(self, station_codes: list):
method get_kpi_station_year (line 112) | def get_kpi_station_year(self, station_codes: list):
method get_dev_list (line 126) | def get_dev_list(self, station_codes: list):
method get_dev_real_kpi (line 151) | def get_dev_real_kpi(self, device_ids: list, type_id: int):
method _do_call (line 161) | def _do_call(self, url: str, json: dict):
class FusionSolarOpenApiError (line 208) | class FusionSolarOpenApiError(Exception):
class FusionSolarOpenApiAccessFrequencyTooHighError (line 212) | class FusionSolarOpenApiAccessFrequencyTooHighError(FusionSolarOpenApiEr...
class FusionSolarOpenApiErrorInvalidAccessToCurrentInterfaceError (line 216) | class FusionSolarOpenApiErrorInvalidAccessToCurrentInterfaceError(Fusion...
FILE: custom_components/fusion_solar/fusion_solar/openapi/station.py
class FusionSolarStation (line 4) | class FusionSolarStation:
method __init__ (line 5) | def __init__(
method device_info (line 27) | def device_info(self):
method readable_name (line 38) | def readable_name(self):
FILE: custom_components/fusion_solar/fusion_solar/power_entity.py
class FusionSolarPowerEntity (line 8) | class FusionSolarPowerEntity(CoordinatorEntity, Entity):
method __init__ (line 11) | def __init__(
method device_class (line 30) | def device_class(self):
method unique_id (line 34) | def unique_id(self) -> str:
method name (line 38) | def name(self):
method state (line 42) | def state(self):
method unit_of_measurement (line 53) | def unit_of_measurement(self):
method device_info (line 57) | def device_info(self) -> dict:
method _handle_coordinator_update (line 61) | def _handle_coordinator_update(self):
class FusionSolarPowerEntityRealtime (line 79) | class FusionSolarPowerEntityRealtime(FusionSolarPowerEntity):
class FusionSolarPowerEntityRealtimeInWatt (line 83) | class FusionSolarPowerEntityRealtimeInWatt(FusionSolarPowerEntity):
method unit_of_measurement (line 85) | def unit_of_measurement(self):
FILE: custom_components/fusion_solar/fusion_solar/realtime_device_data_sensor.py
class FusionSolarRealtimeDeviceDataSensor (line 14) | class FusionSolarRealtimeDeviceDataSensor(CoordinatorEntity, SensorEntity):
method __init__ (line 17) | def __init__(
method unique_id (line 34) | def unique_id(self) -> str:
method name (line 38) | def name(self) -> str:
method native_value (line 42) | def native_value(self) -> float:
method device_info (line 53) | def device_info(self) -> dict:
method _handle_coordinator_update (line 57) | def _handle_coordinator_update(self):
class FusionSolarRealtimeDeviceDataTranslatedSensor (line 75) | class FusionSolarRealtimeDeviceDataTranslatedSensor(FusionSolarRealtimeD...
method state (line 77) | def state(self) -> int:
method translation_key (line 88) | def translation_key(self) -> str:
class FusionSolarRealtimeDeviceDataVoltageSensor (line 92) | class FusionSolarRealtimeDeviceDataVoltageSensor(FusionSolarRealtimeDevi...
method device_class (line 94) | def device_class(self) -> str:
method native_unit_of_measurement (line 98) | def native_unit_of_measurement(self) -> str:
method state_class (line 102) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataCurrentSensor (line 106) | class FusionSolarRealtimeDeviceDataCurrentSensor(FusionSolarRealtimeDevi...
method device_class (line 108) | def device_class(self) -> str:
method native_unit_of_measurement (line 112) | def native_unit_of_measurement(self) -> str:
method state_class (line 116) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataEnergySensor (line 120) | class FusionSolarRealtimeDeviceDataEnergySensor(FusionSolarRealtimeDevic...
method device_class (line 122) | def device_class(self) -> str:
method native_unit_of_measurement (line 126) | def native_unit_of_measurement(self) -> str:
method state_class (line 130) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor (line 134) | class FusionSolarRealtimeDeviceDataEnergyTotalIncreasingSensor(FusionSol...
method state_class (line 136) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataTemperatureSensor (line 140) | class FusionSolarRealtimeDeviceDataTemperatureSensor(FusionSolarRealtime...
method device_class (line 142) | def device_class(self) -> str:
method native_unit_of_measurement (line 146) | def native_unit_of_measurement(self) -> str:
method state_class (line 150) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataPowerFactorSensor (line 154) | class FusionSolarRealtimeDeviceDataPowerFactorSensor(FusionSolarRealtime...
method device_class (line 156) | def device_class(self) -> str:
method native_unit_of_measurement (line 160) | def native_unit_of_measurement(self) -> str:
method state_class (line 164) | def state_class(self) -> str:
method native_value (line 168) | def native_value(self) -> str:
class FusionSolarRealtimeDeviceDataFrequencySensor (line 177) | class FusionSolarRealtimeDeviceDataFrequencySensor(FusionSolarRealtimeDe...
method device_class (line 179) | def device_class(self) -> str:
method native_unit_of_measurement (line 183) | def native_unit_of_measurement(self) -> str:
method state_class (line 187) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataPowerSensor (line 191) | class FusionSolarRealtimeDeviceDataPowerSensor(FusionSolarRealtimeDevice...
method device_class (line 193) | def device_class(self) -> str:
method native_unit_of_measurement (line 197) | def native_unit_of_measurement(self) -> str:
method state_class (line 201) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataPowerInWattSensor (line 205) | class FusionSolarRealtimeDeviceDataPowerInWattSensor(FusionSolarRealtime...
method native_unit_of_measurement (line 207) | def native_unit_of_measurement(self) -> str:
class FusionSolarRealtimeDeviceDataReactivePowerSensor (line 211) | class FusionSolarRealtimeDeviceDataReactivePowerSensor(FusionSolarRealti...
method device_class (line 213) | def device_class(self) -> str:
method native_unit_of_measurement (line 217) | def native_unit_of_measurement(self) -> str:
method state_class (line 221) | def state_class(self) -> str:
method native_value (line 225) | def native_value(self) -> float:
class FusionSolarRealtimeDeviceDataReactivePowerInVarSensor (line 235) | class FusionSolarRealtimeDeviceDataReactivePowerInVarSensor(FusionSolarR...
method native_unit_of_measurement (line 237) | def native_unit_of_measurement(self) -> str:
class FusionSolarRealtimeDeviceDataApparentPowerSensor (line 241) | class FusionSolarRealtimeDeviceDataApparentPowerSensor(FusionSolarRealti...
method device_class (line 243) | def device_class(self) -> str:
method native_unit_of_measurement (line 247) | def native_unit_of_measurement(self) -> str:
method state_class (line 251) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataWindSpeedSensor (line 255) | class FusionSolarRealtimeDeviceDataWindSpeedSensor(FusionSolarRealtimeDe...
method device_class (line 257) | def device_class(self) -> str:
method native_unit_of_measurement (line 261) | def native_unit_of_measurement(self) -> str:
method state_class (line 265) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataBatterySensor (line 269) | class FusionSolarRealtimeDeviceDataBatterySensor(FusionSolarRealtimeDevi...
method device_class (line 271) | def device_class(self) -> str:
method native_unit_of_measurement (line 275) | def native_unit_of_measurement(self) -> str:
method state_class (line 279) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataTimestampSensor (line 283) | class FusionSolarRealtimeDeviceDataTimestampSensor(FusionSolarRealtimeDe...
method device_class (line 285) | def device_class(self) -> str:
method state (line 289) | def state(self) -> datetime:
class FusionSolarRealtimeDeviceDataPercentageSensor (line 298) | class FusionSolarRealtimeDeviceDataPercentageSensor(FusionSolarRealtimeD...
method native_unit_of_measurement (line 300) | def native_unit_of_measurement(self) -> str | None:
method state_class (line 304) | def state_class(self) -> str:
class FusionSolarRealtimeDeviceDataBinarySensor (line 308) | class FusionSolarRealtimeDeviceDataBinarySensor(CoordinatorEntity, Binar...
method __init__ (line 311) | def __init__(
method unique_id (line 328) | def unique_id(self) -> str:
method name (line 332) | def name(self) -> str:
method device_info (line 336) | def device_info(self) -> dict:
method _handle_coordinator_update (line 340) | def _handle_coordinator_update(self):
class FusionSolarRealtimeDeviceDataStateBinarySensor (line 356) | class FusionSolarRealtimeDeviceDataStateBinarySensor(FusionSolarRealtime...
method device_class (line 358) | def device_class(self) -> str:
method is_on (line 362) | def is_on(self) -> bool:
FILE: custom_components/fusion_solar/fusion_solar/station_attribute_entity.py
class FusionSolarStationAttributeEntity (line 6) | class FusionSolarStationAttributeEntity(Entity):
method __init__ (line 7) | def __init__(
method unique_id (line 22) | def unique_id(self) -> str:
method name (line 26) | def name(self):
method state (line 30) | def state(self):
method device_info (line 34) | def device_info(self) -> dict:
method entity_category (line 38) | def entity_category(self) -> str:
method should_poll (line 42) | def should_poll(self) -> bool:
class FusionSolarStationCapacityEntity (line 46) | class FusionSolarStationCapacityEntity(FusionSolarStationAttributeEntity):
class FusionSolarStationContactPersonEntity (line 50) | class FusionSolarStationContactPersonEntity(FusionSolarStationAttributeE...
class FusionSolarStationContactPersonPhoneEntity (line 54) | class FusionSolarStationContactPersonPhoneEntity(FusionSolarStationAttri...
class FusionSolarStationAddressEntity (line 58) | class FusionSolarStationAddressEntity(FusionSolarStationAttributeEntity):
FILE: custom_components/fusion_solar/fusion_solar/year_plant_data_entity.py
class FusionSolarYearPlantDataSensor (line 12) | class FusionSolarYearPlantDataSensor(CoordinatorEntity, SensorEntity):
method __init__ (line 15) | def __init__(
method unique_id (line 26) | def unique_id(self) -> str:
method native_value (line 30) | def native_value(self) -> float:
method device_info (line 53) | def device_info(self) -> dict:
class FusionSolarYearPlantDataInstalledCapacitySensor (line 57) | class FusionSolarYearPlantDataInstalledCapacitySensor(FusionSolarYearPla...
method name (line 61) | def name(self) -> str:
method device_class (line 65) | def device_class(self) -> str:
method native_unit_of_measurement (line 69) | def native_unit_of_measurement(self) -> str:
method state_class (line 73) | def state_class(self) -> str:
class FusionSolarYearPlantDataRadiationIntensitySensor (line 77) | class FusionSolarYearPlantDataRadiationIntensitySensor(FusionSolarYearPl...
method name (line 81) | def name(self) -> str:
method device_class (line 85) | def device_class(self) -> str:
method native_unit_of_measurement (line 89) | def native_unit_of_measurement(self) -> str:
method state_class (line 93) | def state_class(self) -> str:
method state (line 97) | def state(self) -> float:
class FusionSolarYearPlantDataTheoryPowerSensor (line 106) | class FusionSolarYearPlantDataTheoryPowerSensor(FusionSolarYearPlantData...
method name (line 110) | def name(self) -> str:
method device_class (line 114) | def device_class(self) -> str:
method native_unit_of_measurement (line 118) | def native_unit_of_measurement(self) -> str:
method state_class (line 122) | def state_class(self) -> str:
class FusionSolarYearPlantDataPerformanceRatioSensor (line 126) | class FusionSolarYearPlantDataPerformanceRatioSensor(FusionSolarYearPlan...
method name (line 130) | def name(self) -> str:
method device_class (line 134) | def device_class(self) -> str:
method native_unit_of_measurement (line 138) | def native_unit_of_measurement(self) -> str:
method state_class (line 142) | def state_class(self) -> str:
class FusionSolarYearPlantDataInverterPowerSensor (line 146) | class FusionSolarYearPlantDataInverterPowerSensor(FusionSolarYearPlantDa...
method name (line 150) | def name(self) -> str:
method device_class (line 154) | def device_class(self) -> str:
method native_unit_of_measurement (line 158) | def native_unit_of_measurement(self) -> str:
method state_class (line 162) | def state_class(self) -> str:
class FusionSolarYearPlantDataOngridPowerSensor (line 166) | class FusionSolarYearPlantDataOngridPowerSensor(FusionSolarYearPlantData...
method name (line 170) | def name(self) -> str:
method device_class (line 174) | def device_class(self) -> str:
method native_unit_of_measurement (line 178) | def native_unit_of_measurement(self) -> str:
method state_class (line 182) | def state_class(self) -> str:
class FusionSolarYearPlantDataUsePowerSensor (line 186) | class FusionSolarYearPlantDataUsePowerSensor(FusionSolarYearPlantDataSen...
method name (line 190) | def name(self) -> str:
method device_class (line 194) | def device_class(self) -> str:
method native_unit_of_measurement (line 198) | def native_unit_of_measurement(self) -> str:
method state_class (line 202) | def state_class(self) -> str:
class FusionSolarYearPlantDataPowerProfitSensor (line 206) | class FusionSolarYearPlantDataPowerProfitSensor(FusionSolarYearPlantData...
method name (line 210) | def name(self) -> str:
method device_class (line 214) | def device_class(self) -> str:
method state_class (line 218) | def state_class(self) -> str:
class FusionSolarYearPlantDataPerpowerRatioSensor (line 222) | class FusionSolarYearPlantDataPerpowerRatioSensor(FusionSolarYearPlantDa...
method name (line 226) | def name(self) -> str:
method state_class (line 230) | def state_class(self) -> str:
class FusionSolarYearPlantDataReductionTotalCo2Sensor (line 234) | class FusionSolarYearPlantDataReductionTotalCo2Sensor(FusionSolarYearPla...
method name (line 238) | def name(self) -> str:
method native_unit_of_measurement (line 242) | def native_unit_of_measurement(self) -> str:
method state_class (line 246) | def state_class(self) -> str:
method native_value (line 250) | def native_value(self) -> float:
method icon (line 259) | def icon(self) -> str | None:
class FusionSolarYearPlantDataReductionTotalCoalSensor (line 263) | class FusionSolarYearPlantDataReductionTotalCoalSensor(FusionSolarYearPl...
method name (line 267) | def name(self) -> str:
method native_unit_of_measurement (line 271) | def native_unit_of_measurement(self) -> str:
method state_class (line 275) | def state_class(self) -> str:
method native_value (line 279) | def native_value(self) -> float:
method icon (line 288) | def icon(self) -> str | None:
class FusionSolarYearPlantDataReductionTotalTreeSensor (line 292) | class FusionSolarYearPlantDataReductionTotalTreeSensor(FusionSolarYearPl...
method name (line 296) | def name(self) -> str:
method state_class (line 300) | def state_class(self) -> str:
method icon (line 304) | def icon(self) -> str | None:
class FusionSolarBackwardsCompatibilityTotalCurrentYear (line 309) | class FusionSolarBackwardsCompatibilityTotalCurrentYear(FusionSolarYearP...
method unique_id (line 311) | def unique_id(self) -> str:
method name (line 315) | def name(self) -> str:
FILE: custom_components/fusion_solar/sensor.py
function filter_for_enabled_stations (line 59) | def filter_for_enabled_stations(station, device_registry):
function add_entities_for_kiosk (line 68) | async def add_entities_for_kiosk(hass, async_add_entities, kiosk: Fusion...
function add_entities_for_stations (line 151) | async def add_entities_for_stations(hass, async_add_entities, stations, ...
function _add_entities_for_stations_real_kpi_data (line 667) | async def _add_entities_for_stations_real_kpi_data(hass, async_add_entit...
function _add_entities_for_stations_year_kpi_data (line 753) | async def _add_entities_for_stations_year_kpi_data(hass, async_add_entit...
function _add_static_entities_for_devices (line 826) | async def _add_static_entities_for_devices(async_add_entities, devices):
function async_setup_entry (line 862) | async def async_setup_entry(hass, config_entry, async_add_entities):
function async_setup_platform (line 891) | async def async_setup_platform(hass, config, async_add_entities, discove...
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (190K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 21,
"preview": "github: tijsverkoyen\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 1392,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n**Describe the bu"
},
{
"path": ".github/workflows/hacs.yaml",
"chars": 266,
"preview": "name: HACS Action\n\non:\n push:\n pull_request:\n schedule:\n - cron: \"0 0 * * *\"\n\njobs:\n hacs:\n name: HACS Action\n"
},
{
"path": ".github/workflows/hassfest.yaml",
"chars": 241,
"preview": "name: Validate with hassfest\n\non:\n push:\n pull_request:\n schedule:\n - cron: \"0 0 * * *\"\n\njobs:\n validate:\n run"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2020 Tijs Verkoyen\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "README.md",
"chars": 10120,
"preview": "# Home Assistant FusionSolar Integration\n\n[](https://"
},
{
"path": "custom_components/fusion_solar/__init__.py",
"chars": 1295,
"preview": "\"\"\"\nCustom integration to integrate FusionSolar with Home Assistant.\n\"\"\"\nfrom homeassistant.core import HomeAssistant\nfr"
},
{
"path": "custom_components/fusion_solar/config_flow.py",
"chars": 4205,
"preview": "from typing import Any, Dict, Optional\n\nfrom homeassistant import config_entries\nfrom homeassistant.const import CONF_NA"
},
{
"path": "custom_components/fusion_solar/const.py",
"chars": 820,
"preview": "\"\"\"Constants for FusionSolar.\"\"\"\n# Base constants\nDOMAIN = 'fusion_solar'\n\n# Configuration\nCONF_KIOSKS = 'kiosks'\nCONF_O"
},
{
"path": "custom_components/fusion_solar/device_real_kpi_coordinator.py",
"chars": 4965,
"preview": "from datetime import timedelta\nimport math\nimport logging\n\nfrom homeassistant.core import callback\nfrom homeassistant.he"
},
{
"path": "custom_components/fusion_solar/fusion_solar/const.py",
"chars": 1996,
"preview": "# Fusion Solar API response attributes\nATTR_AID_TYPE = 'aidType'\nATTR_BUILD_STATE = 'buildState'\nATTR_CAPACITY = 'capaci"
},
{
"path": "custom_components/fusion_solar/fusion_solar/device_attribute_entity.py",
"chars": 1275,
"preview": "from homeassistant.helpers.entity import Entity, EntityCategory\n\nfrom .openapi.device import FusionSolarDevice\nfrom ..co"
},
{
"path": "custom_components/fusion_solar/fusion_solar/energy_sensor.py",
"chars": 4519,
"preview": "import logging\nimport math\n\nfrom homeassistant.helpers.update_coordinator import CoordinatorEntity\nfrom homeassistant.co"
},
{
"path": "custom_components/fusion_solar/fusion_solar/kiosk/kiosk.py",
"chars": 594,
"preview": "import re\nimport logging\n\nfrom urllib.parse import urlparse\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass FusionSolarKi"
},
{
"path": "custom_components/fusion_solar/fusion_solar/kiosk/kiosk_api.py",
"chars": 1474,
"preview": "\"\"\"API client for FusionSolar Kiosk.\"\"\"\nimport logging\nimport html\nimport json\n\nfrom ..const import (\n ATTR_DATA,\n "
},
{
"path": "custom_components/fusion_solar/fusion_solar/lifetime_plant_data_entity.py",
"chars": 5611,
"preview": "import logging\n\nfrom homeassistant.helpers.update_coordinator import CoordinatorEntity\nfrom homeassistant.components.sen"
},
{
"path": "custom_components/fusion_solar/fusion_solar/openapi/device.py",
"chars": 2742,
"preview": "from ...const import DOMAIN\n\n\nclass FusionSolarDevice:\n def __init__(\n self,\n device_id: str,\n "
},
{
"path": "custom_components/fusion_solar/fusion_solar/openapi/openapi_api.py",
"chars": 7786,
"preview": "\"\"\"API client for FusionSolar OpenAPI.\"\"\"\nimport logging\nimport time\nimport datetime\nfrom datetime import timezone\n\nfrom"
},
{
"path": "custom_components/fusion_solar/fusion_solar/openapi/station.py",
"chars": 1115,
"preview": "from ...const import DOMAIN\n\n\nclass FusionSolarStation:\n def __init__(\n self,\n code: str,\n "
},
{
"path": "custom_components/fusion_solar/fusion_solar/power_entity.py",
"chars": 2421,
"preview": "from homeassistant.core import callback\nfrom homeassistant.const import UnitOfPower\nfrom homeassistant.components.sensor"
},
{
"path": "custom_components/fusion_solar/fusion_solar/realtime_device_data_sensor.py",
"chars": 10704,
"preview": "import datetime\n\nfrom homeassistant.core import callback\nfrom homeassistant.helpers.update_coordinator import Coordinato"
},
{
"path": "custom_components/fusion_solar/fusion_solar/station_attribute_entity.py",
"chars": 1530,
"preview": "from homeassistant.helpers.entity import Entity, EntityCategory\n\nfrom .openapi.station import FusionSolarStation\nfrom .."
},
{
"path": "custom_components/fusion_solar/fusion_solar/year_plant_data_entity.py",
"chars": 8504,
"preview": "import logging\n\nfrom homeassistant.helpers.update_coordinator import CoordinatorEntity\nfrom homeassistant.components.sen"
},
{
"path": "custom_components/fusion_solar/manifest.json",
"chars": 416,
"preview": "{\n \"domain\": \"fusion_solar\",\n \"name\": \"FusionSolar\",\n \"codeowners\": [\n \"@tijsVerkoyen\"\n ],\n \"config_flow\": true,"
},
{
"path": "custom_components/fusion_solar/sensor.py",
"chars": 54442,
"preview": "\"\"\"FusionSolar sensor.\"\"\"\nimport homeassistant.helpers.config_validation as cv\nimport logging\nimport voluptuous as vol\n\n"
},
{
"path": "custom_components/fusion_solar/strings.json",
"chars": 3654,
"preview": "{\n \"title\": \"Fusion Solar\",\n \"config\": {\n \"error\": {\n \"invalid_type\": \"Invalid type, only kiosk or openapi are"
},
{
"path": "custom_components/fusion_solar/translations/de.json",
"chars": 4718,
"preview": "{\n \"title\": \"Fusion Solar\",\n \"config\": {\n \"error\": {\n \"invalid_type\": \"Ung\\u00fcltiger Typ, nur "
},
{
"path": "custom_components/fusion_solar/translations/en.json",
"chars": 3654,
"preview": "{\n \"title\": \"Fusion Solar\",\n \"config\": {\n \"error\": {\n \"invalid_type\": \"Invalid type, only kiosk or openapi are"
},
{
"path": "custom_components/fusion_solar/translations/es.json",
"chars": 5021,
"preview": "{\n \"title\": \"Fusion Solar\",\n \"config\": {\n \"error\": {\n \"invalid_type\": \"Tipo inv\\u00e1lido, solo "
},
{
"path": "custom_components/fusion_solar/translations/fr.json",
"chars": 5258,
"preview": "{\n \"title\": \"Fusion Solar\",\n \"config\": {\n \"error\": {\n \"invalid_type\": \"Type invalide, seuls kios"
},
{
"path": "custom_components/fusion_solar/translations/it.json",
"chars": 4800,
"preview": "{\n \"title\": \"Fusion Solar\",\n \"config\": {\n \"error\": {\n \"invalid_type\": \"Tipo non valido, sono con"
},
{
"path": "custom_components/fusion_solar/translations/pt.json",
"chars": 5063,
"preview": "{\n \"title\": \"Fusion Solar\",\n \"config\": {\n \"error\": {\n \"invalid_type\": \"Tipo inv\\u00e1lido, apena"
},
{
"path": "docs/postman_collection.json",
"chars": 13531,
"preview": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"34276b21-5c16-49da-9369-23e28183ee63\",\n\t\t\"name\": \"FusionSolar\",\n\t\t\"schema\": \"https://sche"
},
{
"path": "hacs.json",
"chars": 54,
"preview": "{\n \"name\": \"Fusion Solar\",\n \"render_readme\": true\n}\n"
}
]
About this extraction
This page contains the full source code of the tijsverkoyen/HomeAssistant-FusionSolar GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (171.2 KB), approximately 42.4k tokens, and a symbol index with 291 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.