Skip to content

Commit

Permalink
Merge pull request #2 from tijsverkoyen/openapi
Browse files Browse the repository at this point in the history
OpenAPI implementation
  • Loading branch information
tijsverkoyen authored Oct 25, 2022
2 parents 5933fad + 2561be5 commit 4a6bd01
Show file tree
Hide file tree
Showing 14 changed files with 513 additions and 37 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ The configuration happens in the configuration flow when you add the integration
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.**

If you need more accurate information you should use the OpenAPI mode.

### OpenAPI
You will need an OpenAPI account from Huawei for this to work. [More information](https://forum.huawei.com/enterprise/en/communicate-with-fusionsolar-through-an-openapi-account/thread/591478-100027)

The integration updates the total yields (current day, current month, current year, lifetime) every 10 minutes.
The realtime data is updated every minute.
34 changes: 31 additions & 3 deletions custom_components/fusion_solar/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import Any, Dict, Optional

from homeassistant import config_entries
from homeassistant.const import CONF_NAME, CONF_URL
from .const import DOMAIN, CONF_KIOSKS, CONF_TYPE, CONF_TYPE_KIOSK, CONF_TYPE_OPENAPI
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
Expand All @@ -13,6 +15,7 @@
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):
Expand Down Expand Up @@ -76,11 +79,36 @@ async def async_step_openapi(self, user_input: Optional[Dict[str, Any]] = None):
_LOGGER.debug(f'async_step_openapi: {user_input}')
errors: Dict[str, str] = {}

errors['base'] = 'not_implemented'
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,
)
1 change: 1 addition & 0 deletions custom_components/fusion_solar/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# Configuration
CONF_KIOSKS = 'kiosks'
CONF_OPENAPI_CREDENTIALS = 'credentials'
CONF_TYPE = 'type'
CONF_TYPE_KIOSK = 'kiosk'
CONF_TYPE_OPENAPI = 'openapi'
Expand Down
28 changes: 27 additions & 1 deletion custom_components/fusion_solar/fusion_solar/const.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
# Fusion Solar Kiosk API response attributes
# Fusion Solar API response attributes
ATTR_DATA = 'data'
ATTR_DATA_REALKPI = 'realKpi'
ATTR_DATA_COLLECT_TIME = 'collectTime'
ATTR_FAIL_CODE = 'failCode'
ATTR_MESSAGE = 'message'
ATTR_SUCCESS = 'success'
ATTR_STATION_CODE = 'stationCode'
ATTR_STATION_NAME = 'stationName'
ATTR_PARAMS = 'params'
ATTR_PARAMS_CURRENT_TIME = 'currentTime'
ATTR_DEVICE_ID = 'id'
ATTR_DEVICE_NAME = 'devName'
ATTR_DEVICE_STATION_CODE = 'stationCode'
ATTR_DEVICE_ESN_CODE = 'esnCode'
ATTR_DEVICE_TYPE_ID = 'devTypeId'
ATTR_DEVICE_INVERTER_TYPE = 'invType'
ATTR_DEVICE_SOFTWARE_VERSION = 'softwareVersion'

# 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_DEV_ID = 'devId'
ATTR_DEVICE_REAL_KPI_DATA_ITEM_MAP = 'dataItemMap'
ATTR_DEVICE_REAL_KPI_ACTIVE_POWER = 'active_power'

PARAM_DEVICE_TYPE_ID_RESIDENTIAL_INVERTER = 38
18 changes: 12 additions & 6 deletions custom_components/fusion_solar/fusion_solar/energy_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING, SensorEntity
from homeassistant.const import DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR

from .const import ATTR_TOTAL_LIFETIME_ENERGY, ATTR_DATA_REALKPI
from .const import ATTR_TOTAL_LIFETIME_ENERGY, ATTR_STATION_REAL_KPI_TOTAL_LIFETIME_ENERGY

_LOGGER = logging.getLogger(__name__)

Expand All @@ -26,14 +26,16 @@ def __init__(
unique_id,
name,
attribute,
data_name
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:
Expand All @@ -57,7 +59,7 @@ def state(self) -> float:

if entity is not None:
current_value = entity.state
new_value = self.coordinator.data[self._data_name][ATTR_DATA_REALKPI][self._attribute]
new_value = self.coordinator.data[self._data_name][self._attribute]

if not isfloat(new_value):
_LOGGER.warning(f'{self.entity_id}: new value ({new_value}) is not a float, so not updating.')
Expand All @@ -72,13 +74,13 @@ def state(self) -> float:
f'{self.entity_id}: new value ({new_value}) is smaller then current value ({entity.state}), so not updating.')
return float(current_value)

if ATTR_DATA_REALKPI not in self.coordinator.data[self._data_name]:
if self._data_name not in self.coordinator.data:
return None

if self._attribute not in self.coordinator.data[self._data_name][ATTR_DATA_REALKPI]:
if self._attribute not in self.coordinator.data[self._data_name]:
return None

return float(self.coordinator.data[self._data_name][ATTR_DATA_REALKPI][self._attribute])
return float(self.coordinator.data[self._data_name][self._attribute])

@property
def unit_of_measurement(self) -> str:
Expand All @@ -96,6 +98,10 @@ def native_value(self) -> str:
def native_unit_of_measurement(self) -> str:
return self.unit_of_measurement

@property
def device_info(self) -> dict:
return self._device_info


class FusionSolarEnergySensorTotalCurrentDay(FusionSolarEnergySensor):
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
_LOGGER = logging.getLogger(__name__)


class Kiosk:
class FusionSolarKiosk:
def __init__(self, url, name):
self.url = url
self.name = name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import html
import json

from .const import (
from ..const import (
ATTR_DATA,
ATTR_FAIL_CODE,
ATTR_SUCCESS,
Expand All @@ -15,7 +15,7 @@
_LOGGER = logging.getLogger(__name__)


class FusionSolarKioksApi:
class FusionSolarKioskApi:
def __init__(self, host):
self._host = host

Expand All @@ -27,7 +27,6 @@ def getRealTimeKpi(self, id: str):

try:
response = get(url, headers=headers)
# _LOGGER.debug(response.text)
jsonData = response.json()

if not jsonData[ATTR_SUCCESS]:
Expand Down
42 changes: 42 additions & 0 deletions custom_components/fusion_solar/fusion_solar/openapi/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from ...const import DOMAIN


class FusionSolarDevice:
def __init__(self, id: str, name: str, station_code: str, esn_code: str, type_id: str, inverter_type,
software_version: str):
self.device_id = 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

@property
def model(self) -> str:
if self.type_id == 1:
return 'String inverter'
if self.type_id == 10:
return 'EMI'
if self.type_id == 17:
return 'Grid meter'
if self.type_id == 38:
return f'Residential inverter {self.inverter_type}'
if self.type_id == 39:
return 'Battery'
if self.type_id == 47:
return 'Power Sensor'

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)
}
Loading

0 comments on commit 4a6bd01

Please sign in to comment.