From c0fde629aa86827bd5c6fcae07a131714ab8fd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20T=C5=99e=C5=A1t=C3=ADk?= Date: Thu, 19 Dec 2024 14:22:18 +0100 Subject: [PATCH 1/4] #42 adding HAC charger type Basic entity mapping. Mostly just identification of new charger. --- custom_components/solax_http/__init__.py | 20 +- custom_components/solax_http/button.py | 45 +- custom_components/solax_http/const.py | 104 +- custom_components/solax_http/coordinator.py | 95 +- .../solax_http/entity_definitions.py | 850 +++++++++++++ custom_components/solax_http/manifest.json | 2 +- custom_components/solax_http/number.py | 35 +- custom_components/solax_http/plugin_base.py | 55 + .../solax_http/plugin_factory.py | 146 +++ .../solax_http/plugin_solax_ev_charger.py | 1074 ++--------------- .../solax_http/plugin_solax_ev_charger_g2.py | 217 ++++ custom_components/solax_http/select.py | 36 +- custom_components/solax_http/sensor.py | 18 +- custom_components/solax_http/time.py | 35 +- 14 files changed, 1583 insertions(+), 1149 deletions(-) create mode 100644 custom_components/solax_http/entity_definitions.py create mode 100644 custom_components/solax_http/plugin_base.py create mode 100644 custom_components/solax_http/plugin_factory.py create mode 100644 custom_components/solax_http/plugin_solax_ev_charger_g2.py diff --git a/custom_components/solax_http/__init__.py b/custom_components/solax_http/__init__.py index 5931aa1..966dd31 100644 --- a/custom_components/solax_http/__init__.py +++ b/custom_components/solax_http/__init__.py @@ -2,17 +2,15 @@ import logging -from . import plugin_solax_ev_charger -from .coordinator import SolaxHttpUpdateCoordinator from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant -from .const import ( - DOMAIN, -) +from .const import CONF_SN, DOMAIN +from .coordinator import SolaxHttpUpdateCoordinator +from .plugin_factory import PluginFactory -PLATFORMS = ["button", "time", "number", "select", "sensor"] +PLATFORMS = ["button", "number", "select", "sensor", "time"] _LOGGER = logging.getLogger(__name__) @@ -27,13 +25,13 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up a SolaX Http.""" - _LOGGER.debug(f"setup entries - data: {entry.data}, options: {entry.options}") + _LOGGER.debug("setup entries - data: %s, options: %s", entry.data, entry.options) config = entry.options name = config[CONF_NAME] - _LOGGER.debug(f"Setup {DOMAIN}.{name}") + _LOGGER.debug("Setup %s.%s", DOMAIN, name) - plugin = plugin_solax_ev_charger.get_plugin_instance() + plugin = await PluginFactory.get_plugin_instance(config[CONF_HOST], config[CONF_SN]) coordinator = SolaxHttpUpdateCoordinator(hass, entry, plugin) await coordinator.async_config_entry_first_refresh() @@ -61,4 +59,4 @@ async def async_unload_entry(hass: HomeAssistant, entry): async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener, called when the config entry options are changed.""" - await hass.config_entries.async_reload(entry.entry_id) \ No newline at end of file + await hass.config_entries.async_reload(entry.entry_id) diff --git a/custom_components/solax_http/button.py b/custom_components/solax_http/button.py index f892485..51078c6 100644 --- a/custom_components/solax_http/button.py +++ b/custom_components/solax_http/button.py @@ -1,20 +1,22 @@ -from homeassistant.helpers.update_coordinator import CoordinatorEntity +import logging +from typing import Optional + from homeassistant.components.button import ButtonEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback -from .const import ATTR_MANUFACTURER, DOMAIN -from .const import BaseHttpButtonEntityDescription -from .const import plugin_base +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTR_MANUFACTURER, DOMAIN, BaseHttpButtonEntityDescription from .coordinator import SolaxHttpUpdateCoordinator -from typing import Any, Dict, Optional -import logging +from .plugin_base import plugin_base _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities) -> None: + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities) -> None: name = entry.options[CONF_NAME] coordinator: SolaxHttpUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - plugin:plugin_base=coordinator.plugin + plugin: plugin_base = coordinator.plugin device_info = { "identifiers": {(DOMAIN, name)}, @@ -24,29 +26,26 @@ async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities) -> No entities = [] for button_info in plugin.BUTTON_TYPES: - if plugin.matchWithMask(button_info.allowedtypes,button_info.blacklist): - button = SolaXHttpButton( - coordinator, - name, - device_info, - button_info - ) + if plugin.matchWithMask(button_info.allowedtypes, button_info.blacklist): + button = SolaXHttpButton(coordinator, name, device_info, button_info) entities.append(button) async_add_entities(entities) return True + class SolaXHttpButton(CoordinatorEntity, ButtonEntity): """Representation of an SolaX Http button.""" + coordinator: SolaxHttpUpdateCoordinator def __init__( - self, - coordinator:SolaxHttpUpdateCoordinator, - platform_name, - device_info, - description: BaseHttpButtonEntityDescription, + self, + coordinator: SolaxHttpUpdateCoordinator, + platform_name, + device_info, + description: BaseHttpButtonEntityDescription, ) -> None: """Initialize the selector.""" super().__init__(coordinator, context=description) @@ -78,5 +77,7 @@ def unique_id(self) -> Optional[str]: async def async_press(self) -> None: """Write the button value.""" - success=await self.coordinator.write_register(self.entity_description.register, 1) - # await self.coordinator.async_request_refresh() \ No newline at end of file + success = await self.coordinator.write_register( + self.entity_description.register, 1 + ) + # await self.coordinator.async_request_refresh() diff --git a/custom_components/solax_http/const.py b/custom_components/solax_http/const.py index 229421c..fe52202 100644 --- a/custom_components/solax_http/const.py +++ b/custom_components/solax_http/const.py @@ -1,17 +1,13 @@ +"""Module contains constants and base classes for the SolaX HTTP integration.""" + +from dataclasses import dataclass + from homeassistant.components.button import ButtonEntityDescription -from homeassistant.components.switch import SwitchEntityDescription +from homeassistant.components.number import NumberEntityDescription from homeassistant.components.select import SelectEntityDescription +from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.switch import SwitchEntityDescription from homeassistant.components.time import TimeEntityDescription -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.components.number import ( - NumberDeviceClass, - NumberEntityDescription, -) -from dataclasses import dataclass DOMAIN = "solax_http" @@ -30,73 +26,54 @@ S32 = "_int32" -# ==================================== plugin base class ==================================================================== - -@dataclass -class plugin_base: - plugin_name: str - TIME_TYPES: list[TimeEntityDescription] - SENSOR_TYPES: list[SensorEntityDescription] - BUTTON_TYPES: list[ButtonEntityDescription] - NUMBER_TYPES: list[NumberEntityDescription] - SELECT_TYPES: list[SelectEntityDescription] - - invertertype = None - - async def initialize(self)->None: - pass - - def map_data(self, descr, data)->any: - return None - - def map_payload(self, address, payload): - return None - - def matchWithMask(self, entitymask, blacklist = None): - return False - # =================================== base class for sensor entity descriptions ========================================= - @dataclass class BaseHttpSensorEntityDescription(SensorEntityDescription): - """ base class for modbus sensor declarations """ - allowedtypes: int = 0 # overload with ALLDEFAULT from plugin - scale: float = 1 # can be float, dictionary or callable function(initval, descr, datadict) - read_scale_exceptions: list = None # additional scaling when reading from modbus + """Base class for modbus sensor declarations.""" + + allowedtypes: int = 0 # overload with ALLDEFAULT from plugin + scale: float = ( + 1 # can be float, dictionary or callable function(initval, descr, datadict) + ) + read_scale_exceptions: list = None # additional scaling when reading from modbus blacklist: list = None - unit: int = None # e.g. U16 - register: int = -1 # initialize with invalid register + unit: int = None # e.g. U16 + register: int = -1 # initialize with invalid register rounding: int = 1 - value_function: callable = None # value = function(initval, descr, datadict) + value_function: callable = None # value = function(initval, descr, datadict) + @dataclass class BaseHttpButtonEntityDescription(ButtonEntityDescription): - allowedtypes: int = 0 # overload with ALLDEFAULT from plugin + allowedtypes: int = 0 # overload with ALLDEFAULT from plugin register: int = None command: int = None - blacklist: list = None # none or list of serial number prefixes - value_function: callable = None # value = function(initval, descr, datadict) + blacklist: list = None # none or list of serial number prefixes + value_function: callable = None # value = function(initval, descr, datadict) + @dataclass class BaseHttpSwitchEntityDescription(SwitchEntityDescription): - allowedtypes: int = 0 # overload with ALLDEFAULT from plugin + allowedtypes: int = 0 # overload with ALLDEFAULT from plugin register: int = None - blacklist: list = None # none or list of serial number prefixes + blacklist: list = None # none or list of serial number prefixes + @dataclass class BaseHttpSelectEntityDescription(SelectEntityDescription): - allowedtypes: int = 0 # overload with ALLDEFAULT from plugin + allowedtypes: int = 0 # overload with ALLDEFAULT from plugin register: int = None scale: dict = None unit: int = None rounding: int = 1 - reverse_option_dict: dict = None # autocomputed - blacklist: list = None # none or list of serial number prefixes - initvalue: int = None # initial default value for WRITE_DATA_LOCAL entities + reverse_option_dict: dict = None # autocomputed + blacklist: list = None # none or list of serial number prefixes + initvalue: int = None # initial default value for WRITE_DATA_LOCAL entities + @dataclass class BaseHttpNumberEntityDescription(NumberEntityDescription): - allowedtypes: int = 0 # overload with ALLDEFAULT from plugin + allowedtypes: int = 0 # overload with ALLDEFAULT from plugin register: int = None read_scale_exceptions: list = None read_scale: float = 1 @@ -105,16 +82,17 @@ class BaseHttpNumberEntityDescription(NumberEntityDescription): scale: float = 1 rounding: int = 1 state: str = None - max_exceptions: list = None # None or list with structue [ ('U50EC' , 40,) ] - min_exceptions_minus: list = None # same structure as max_exceptions, values are applied with a minus - blacklist: list = None # None or list of serial number prefixes like - initvalue: int = None # initial default value for WRITE_DATA_LOCAL entities - prevent_update: bool = False # if set to True, value will not be re-read/updated with each polling cycle; only when read value changes + max_exceptions: list = None # None or list with structue [ ('U50EC' , 40,) ] + min_exceptions_minus: list = ( + None # same structure as max_exceptions, values are applied with a minus + ) + blacklist: list = None # None or list of serial number prefixes like + initvalue: int = None # initial default value for WRITE_DATA_LOCAL entities + prevent_update: bool = False # if set to True, value will not be re-read/updated with each polling cycle; only when read value changes + @dataclass class BaseHttpTimeEntityDescription(TimeEntityDescription): - allowedtypes: int = 0 # overload with ALLDEFAULT from plugin + allowedtypes: int = 0 # overload with ALLDEFAULT from plugin register: int = None - blacklist: list = None # None or list of serial number prefixes like - - + blacklist: list = None # None or list of serial number prefixes like diff --git a/custom_components/solax_http/coordinator.py b/custom_components/solax_http/coordinator.py index a576ff2..8f5ca55 100644 --- a/custom_components/solax_http/coordinator.py +++ b/custom_components/solax_http/coordinator.py @@ -5,20 +5,30 @@ import aiohttp import async_timeout +from .plugin_base import plugin_base from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_SN, DEFAULT_SCAN_INTERVAL, DOMAIN, REQUEST_REFRESH_DELAY, API_TIMEOUT, plugin_base +from .const import ( + CONF_SN, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + REQUEST_REFRESH_DELAY, + API_TIMEOUT, +) _LOGGER = logging.getLogger(__name__) + class SolaxHttpUpdateCoordinator(DataUpdateCoordinator[None]): config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant, config: ConfigEntry, plugin: plugin_base) -> None: + def __init__( + self, hass: HomeAssistant, config: ConfigEntry, plugin: plugin_base + ) -> None: """Initialize Solax Http API data updater.""" _LOGGER.debug("Setting up coordinator") @@ -49,7 +59,7 @@ async def _async_update_data(self): # up. The API methods themselves have their own timeouts. async with async_timeout.timeout(10 * API_TIMEOUT): # Fetch updates - data=await self.__async_get_data() + data = await self.__async_get_data() if self.plugin.invertertype is None: await self.plugin.initialize(data) return data @@ -67,83 +77,96 @@ async def __async_get_data(self) -> dict: # Grab active context variables to limit data required to be fetched from API # Note: using context is not required if there is no need or ability to limit # data retrieved from API. - contexts=self.async_contexts() + contexts = self.async_contexts() try: realtimeData = await self._read_realtime_data() setData = await self._read_set_data() if setData is None: setData = [] if realtimeData is None: - realtimeData = {'Data':[], 'Information':[]} - except Exception as ex: + realtimeData = {"Data": [], "Information": []} + except Exception: _LOGGER.exception("Something went wrong reading from Http API") data = { - 'Set':{i:v for i,v in enumerate(setData)}, - 'Data':{i:v for i,v in enumerate(realtimeData['Data'])}, - 'Info':{i:v for i,v in enumerate(realtimeData['Information'])} + "Set": dict(enumerate(setData)), + "Data": dict(enumerate(realtimeData["Data"])), + "Info": dict(enumerate(realtimeData["Information"])), } return data async def _read_realtime_data(self): - httpData=None - text=await self._http_post(f'http://{self._host}',f"optType=ReadRealTimeData&pwd={self._sn}") + httpData = None + text = await self._http_post( + f"http://{self._host}", f"optType=ReadRealTimeData&pwd={self._sn}" + ) if text is None: return None if "failed" in text: - _LOGGER.error(f'Failed to read data from http: {text}') + _LOGGER.error("Failed to read data from http: %s", text) return None try: - httpData=json.loads(text) + httpData = json.loads(text) except json.decoder.JSONDecodeError: - _LOGGER.error(f'Failed to decode json: {text}') + _LOGGER.error("Failed to decode json: %s", text) return httpData async def write_register(self, address, payload): - """Write register through http""" - descr=self.plugin.map_payload(address, payload) + """Write register through http.""" + + descr = self.plugin.map_payload(address, payload) if descr is None: return False - resp=await self._http_post(f'http://{self._host}',f'optType=setReg&pwd={self._sn}&data={{"num":1,"Data":{json.dumps(descr)}}}') + resp = await self._http_post( + f"http://{self._host}", + f'optType=setReg&pwd={self._sn}&data={{"num":1,"Data":{json.dumps(descr)}}}', + ) if resp is not None: - _LOGGER.info(f'Received HTTP API response {resp}') + _LOGGER.info("Received HTTP API response %s", resp) return True return False async def _read_set_data(self): - setData=None - text=await self._http_post(f'http://{self._host}',f"optType=ReadSetData&pwd={self._sn}") + setData = None + text = await self._http_post( + f"http://{self._host}", f"optType=ReadSetData&pwd={self._sn}" + ) if text is None: return None if "failed" in text: - _LOGGER.error(f'Failed to read data from http: {text}') + _LOGGER.error("Failed to read data from http: %s", text) return None try: - setData=json.loads(text) + setData = json.loads(text) except json.decoder.JSONDecodeError: - _LOGGER.error(f'Failed to decode json: {text}') + _LOGGER.error("Failed to decode json: %s", text) return setData async def _http_post(self, url, payload, retry=3): try: - connector = aiohttp.TCPConnector(force_close=True,) - async with aiohttp.ClientSession(connector=connector) as session: - async with session.post(url,data=payload) as resp: - if resp.status==200: - return await resp.text() - except asyncio.TimeoutError: - if retry>0: - return await self._http_post(url, payload, retry-1) + connector = aiohttp.TCPConnector( + force_close=True, + ) + async with ( + aiohttp.ClientSession(connector=connector) as session, + session.post(url, data=payload) as resp, + ): + if resp.status == 200: + return await resp.text() + except TimeoutError: + if retry > 0: + return await self._http_post(url, payload, retry - 1) except aiohttp.ServerDisconnectedError: if retry: - return await self._http_post(url, payload, retry-1) + return await self._http_post(url, payload, retry - 1) except aiohttp.client_exceptions.ClientOSError: - if retry>0: - return await self._http_post(url, payload, retry-1) + if retry > 0: + return await self._http_post(url, payload, retry - 1) except Exception as ex: - _LOGGER.exception(f"Error reading from Http. Url: {url}", exc_info=ex) + _LOGGER.exception("Error reading from Http. Url: %s", url, exc_info=ex) return None + class SolaXApiError(Exception): - """Base exception for all SolaX API errors""" \ No newline at end of file + """Base exception for all SolaX API errors.""" diff --git a/custom_components/solax_http/entity_definitions.py b/custom_components/solax_http/entity_definitions.py new file mode 100644 index 0000000..658c2ff --- /dev/null +++ b/custom_components/solax_http/entity_definitions.py @@ -0,0 +1,850 @@ +"""Module contains entity definitions for SolaX EV Charger HTTP integration.""" + +from dataclasses import dataclass +from homeassistant.components.number.const import NumberDeviceClass +from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass +from homeassistant.const import ( + EntityCategory, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, +) + +from .const import ( + S16, + U16, + U32, + BaseHttpButtonEntityDescription, + BaseHttpNumberEntityDescription, + BaseHttpSelectEntityDescription, + BaseHttpSensorEntityDescription, + BaseHttpSwitchEntityDescription, +) + +"""bitmasks definitions to characterize inverters, ogranized by group +these bitmasks are used in entitydeclarations to determine to which inverters the entity applies +within a group, the bits in an entitydeclaration will be interpreted as OR +between groups, an AND condition is applied, so all gruoups must match. +An empty group (group without active flags) evaluates to True. +example: GEN3 | GEN4 | X1 | X3 | EPS +means: any inverter of tyoe (GEN3 or GEN4) and (X1 or X3) and (EPS) +An entity can be declared multiple times (with different bitmasks) if the parameters are different for each inverter type. +""" + +POW7 = 0x0001 +POW11 = 0x0002 +POW22 = 0x0004 +ALL_POW_GROUP = POW7 | POW11 | POW22 + +X1 = 0x0100 +X3 = 0x0200 +ALL_X_GROUP = X1 | X3 + +V10 = 0x0010 +V11 = 0x0020 +V20 = 0x0040 +ALL_VER_GROUP = V10 | V11 | V20 +G1 = V10 | V11 +G2 = V20 + +ALLDEFAULT = 0 + +# ======================= end of bitmask handling code ============================================= + + +# ================================================================================================= + + +@dataclass +class SolaXEVChargerHttpButtonEntityDescription(BaseHttpButtonEntityDescription): + allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice + + +@dataclass +class SolaXEVChargerHttpSwitchEntityDescription(BaseHttpSwitchEntityDescription): + allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice + + +@dataclass +class SolaXEVChargerHttpNumberEntityDescription(BaseHttpNumberEntityDescription): + allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice + + +@dataclass +class SolaXEVChargerHttpTimeEntityDescription(BaseHttpNumberEntityDescription): + allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice + + +@dataclass +class SolaXEVChargerHttpSelectEntityDescription(BaseHttpSelectEntityDescription): + allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice + + +@dataclass +class SolaXEVChargerHttpSensorEntityDescription(BaseHttpSensorEntityDescription): + allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice + + +# ====================================== Computed value functions ================================================= + +# ================================= Button Declarations ============================================================ + +BUTTON_TYPES = [ + SolaXEVChargerHttpButtonEntityDescription( + name="Reset", key="reset", register=0x618, icon="mdi:reset", allowedtypes=G1 + ) + # SolaXEVChargerHttpButtonEntityDescription( + # name = "Sync RTC", + # key = "sync_rtc", + # register = 0x61E, + # icon = "mdi:home-clock", + # value_function = value_function_sync_rtc, + # ), +] + +# ================================= Switch Declarations ============================================================ + +SWITCH_TYPES = [ + # SolaXEVChargerHttpSwitchEntityDescription( + # name = "Boost set", + # key = "boost_set", + # register = 0x61E, + # icon = "mdi:home-clock", + # value_function = value_function_sync_rtc, + # ), +] +# ================================= Time Declarations ============================================================ + +TIME_TYPES = [ + ### + # + # Normal time types + # + ### + SolaXEVChargerHttpTimeEntityDescription( + name="Timed boost start", + key="timed_boost_start", + register=0x634, + allowedtypes=G1, + ), + SolaXEVChargerHttpTimeEntityDescription( + name="Timed boost end", key="timed_boost_end", register=0x636, allowedtypes=G1 + ), + SolaXEVChargerHttpTimeEntityDescription( + name="Smart boost end", key="smart_boost_end", register=0x638, allowedtypes=G1 + ), +] + +# ================================= Number Declarations ============================================================ + +NUMBER_TYPES = [ + ### + # + # Data only number types + # + ### + ### + # + # Normal number types + # + ### + # SolaXEVChargerHttpNumberEntityDescription( + # name = "Datahub Charge Current", + # key = "datahub_charge_current", + # register = 0x624, + # fmt = "f", + # native_min_value = 6, + # native_max_value = 32, + # native_step = 0.1, + # scale = 0.01, + # native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, + # device_class = NumberDeviceClass.CURRENT, + # ), + SolaXEVChargerHttpNumberEntityDescription( + name="Max Charge Current Setting", + key="max_charge_current_setting", + register=0x628, + fmt="f", + native_min_value=6, + native_max_value=16, + native_step=1, + scale=1, + allowedtypes=POW11 | G1, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=NumberDeviceClass.CURRENT, + ), + SolaXEVChargerHttpNumberEntityDescription( + name="Max Charge Current Setting", + key="max_charge_current_setting", + register=0x628, + fmt="f", + native_min_value=6, + native_max_value=32, + native_step=1, + scale=1, + allowedtypes=POW7 | POW22 | G1, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=NumberDeviceClass.CURRENT, + ), + SolaXEVChargerHttpNumberEntityDescription( + name="Smart boost energy", + key="smart_boost_energy", + register=0x63A, + fmt="f", + native_min_value=0, + native_max_value=100, + native_step=1, + scale=1, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=NumberDeviceClass.ENERGY, + allowedtypes=G1, + ), +] + +# ================================= Select Declarations ============================================================ + +SELECT_TYPES = [ + ### + # + # Normal select types + # + ### + SolaXEVChargerHttpSelectEntityDescription( + name="Meter Setting", + key="meter_setting", + register=0x60C, + scale={ + 0: "External CT", + 1: "External Meter", + 2: "Inverter", + }, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:meter-electric", + allowedtypes=G1, + ), + SolaXEVChargerHttpSelectEntityDescription( + name="Charger Use Mode", + key="charger_use_mode", + register=0x60D, + scale={ + 0: "Stop", + 1: "Fast", + 2: "ECO", + 3: "Green", + }, + icon="mdi:dip-switch", + allowedtypes=G1, + ), + SolaXEVChargerHttpSelectEntityDescription( + name="Charger Green Mode Level", + key="charger_green_mode", + register=0x60F, + scale={ + 3: "3A", + 6: "6A", + }, + icon="mdi:dip-switch", + allowedtypes=G1, + ), + SolaXEVChargerHttpSelectEntityDescription( + name="Charger Eco Mode Level", + key="charger_eco_mode", + register=0x60E, + scale={ + 6: "6A", + 10: "10A", + 16: "16A", + 20: "20A", + 25: "25A", + }, + icon="mdi:dip-switch", + allowedtypes=POW7 | POW22 | G1, + ), + SolaXEVChargerHttpSelectEntityDescription( + name="Charger Eco Mode Level", + key="charger_eco_mode", + register=0x60E, + scale={6: "6A", 10: "10A"}, + icon="mdi:dip-switch", + allowedtypes=POW11 | G1, + ), + SolaXEVChargerHttpSelectEntityDescription( + name="Start Charge Mode", + key="start_charge_mode", + register=0x610, + scale={ + 0: "Plug & Charge", + 1: "RFID to Charge", + }, + icon="mdi:lock", + allowedtypes=G1, + ), + SolaXEVChargerHttpSelectEntityDescription( + name="Boost Mode", + key="boost_mode", + register=0x613, + scale={ + 0: "Normal", + 1: "Timer Boost", + 2: "Smart Boost", + }, + icon="mdi:dip-switch", + allowedtypes=G1, + ), + # SolaXEVChargerHttpSelectEntityDescription( + # name = "Device Lock", + # key = "device_lock", + # register = 0x615, + # option_dict = { + # 0: "Unlock", + # 1: "Lock", }, + # icon = "mdi:lock", + # ), + # SolaXEVChargerHttpSelectEntityDescription( + # name = "RFID Program", + # key = "rfid_program", + # register = 0x616, + # option_dict = { + # 0: "Program New", + # 1: "Program Off", }, + # icon = "mdi:dip-switch", + # ), + SolaXEVChargerHttpSelectEntityDescription( + name="Charge Phase", + key="charge_phase", + register=0x625, + scale={ + 0: "Three Phase", + 1: "L1 Phase", + 2: "L2 Phase", + 3: "L3 Phase", + }, + icon="mdi:dip-switch", + allowedtypes=G1, + ), + # SolaXEVChargerHttpSelectEntityDescription( + # name = "Control Command", + # key = "control_command", + # register = 0x627, + # option_dict = { + # 1: "Available", + # 2: "Unavailable", + # 3: "Stop charging", + # 4: "Start Charging", + # 5: "Reserve", + # 6: "Cancel the Reservation", }, + # icon = "mdi:dip-switch", + # ), +] + +# ================================= Sennsor Declarations ============================================================ + +SENSOR_TYPES: list[SolaXEVChargerHttpSensorEntityDescription] = [ + ### + # + # Holding + # + ### + SolaXEVChargerHttpSensorEntityDescription( + name="Charge start time", + key="charge_start_time", + register=0xF001, + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_enabled_default=True, + icon="mdi:clock", + allowedtypes=G1, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="CT Meter Setting", + key="ct_meter_setting", + register=0x60C, + scale={ + 0: "External CT", + 1: "External Meter", + 2: "Inverter", + }, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:meter-electric", + allowedtypes=G1, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charger Use Mode", + key="charger_use_mode", + register=0x60D, + scale={ + 0: "Stop", + 1: "Fast", + 2: "ECO", + 3: "Green", + }, + entity_registry_enabled_default=False, + icon="mdi:dip-switch", + allowedtypes=G1, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Start Charge Mode", + key="start_charge_mode", + register=0x610, + scale={ + 0: "Plug & Charge", + 1: "RFID to Charge", + }, + entity_registry_enabled_default=False, + icon="mdi:lock", + allowedtypes=G1, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Boost Mode", + key="boost_mode", + register=0x613, + scale={ + 0: "Normal", + 1: "Timer Boost", + 2: "Smart Boost", + }, + entity_registry_enabled_default=False, + icon="mdi:dip-switch", + allowedtypes=G1, + ), + # SolaXEVChargerHttpSensorEntityDescription( + # name = "Device Lock", + # key = "device_lock", + # register = 0x615, + # scale = { + # 0: "Unlock", + # 1: "Lock", }, + # entity_registry_enabled_default = False, + # icon = "mdi:lock", + # ), + # SolaXEVChargerHttpSensorEntityDescription( + # name = "RFID Program", + # key = "rfid_program", + # register = 0x616, + # scale = { + # 0: "Program New", + # 1: "Program Off", }, + # entity_registry_enabled_default = False, + # icon = "mdi:dip-switch", + # ), + # SolaXEVChargerHttpSensorEntityDescription( + # name = "RTC", + # key = "rtc", + # register = 0x61E, + # unit = WORDS, + # wordcount = 6, + # scale = value_function_rtc, + # entity_registry_enabled_default = False, + # entity_category = EntityCategory.DIAGNOSTIC, + # icon = "mdi:clock", + # ), + # SolaXEVChargerHttpSensorEntityDescription( + # name = "Datahub Charge Current", + # key = "datahub_charge_current", + # register = 0x624, + # scale = 0.01, + # rounding = 1, + # native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, + # device_class = SensorDeviceClass.CURRENT, + # allowedtypes = HYBRID, + # entity_registry_enabled_default = False, + # ), + # SolaXEVChargerHttpSensorEntityDescription( + # name = "Charge Phase", + # key = "charge_phase", + # register = 0x625, + # scale = { + # 0: "Three Phase", + # 1: "L1 Phase", + # 2: "L2 Phase", + # 3: "L3 Phase", }, + # allowedtypes = X3, + # entity_registry_enabled_default = False, + # icon = "mdi:dip-switch", + # ), + SolaXEVChargerHttpSensorEntityDescription( + name="Max Charge Current", + key="max_charge_current", + register=0x628, + scale=1, + rounding=0, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + allowedtypes=G1, + ), + # SolaXEVChargerHttpSensorEntityDescription( + # name = "Control Command", + # key = "control_command", + # register = 0x627, + # scale = { + # 1: "Available", + # 2: "Unavailable", + # 3: "Stop charging", + # 4: "Start Charging", + # 5: "Reserve", + # 6: "Cancel the Reservation", }, + # entity_registry_enabled_default = False, + # icon = "mdi:dip-switch", + # ), + ### + # + # Input + # + ### + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Voltage", + key="charge_voltage", + register=0x0, + scale=0.01, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + allowedtypes=X1 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Voltage L1", + key="charge_voltage_l1", + register=0x0, + scale=0.01, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + allowedtypes=X3 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Voltage L2", + key="charge_voltage_l2", + register=0x1, + scale=0.01, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + allowedtypes=X3 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Voltage L3", + key="charge_voltage_l3", + register=0x2, + scale=0.01, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + allowedtypes=X3 | G1 | G2, + entity_registry_enabled_default=False, + ), + # SolaXEVChargerHttpSensorEntityDescription( + # name = "Charge PE Voltage", + # key = "charge_pe_voltage", + # register = 0x3, + # scale = 0.01, + # native_unit_of_measurement = UnitOfElectricPotential.VOLT, + # device_class = SensorDeviceClass.VOLTAGE, + # entity_registry_enabled_default = False, + # ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Current", + key="charge_current", + register=0x4, + scale=0.01, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + allowedtypes=X1 | G1 | G2, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Current L1", + key="charge_current_l1", + register=0x4, + scale=0.01, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + allowedtypes=X3 | G1 | G2, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Current L2", + key="charge_current_l2", + register=0x5, + scale=0.01, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + allowedtypes=X3 | G1 | G2, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Current L3", + key="charge_current_l3", + register=0x6, + scale=0.01, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + allowedtypes=X3 | G1 | G2, + ), + # SolaXEVChargerHttpSensorEntityDescription( + # name = "Charge PE Current", + # key = "charge_pe_current", + # register = 0x7, + # register_type = REG_INPUT, + # native_unit_of_measurement = UnitOfElectricCurrent.MILLIAMPERE, + # device_class = SensorDeviceClass.CURRENT, + # entity_registry_enabled_default = False, + # ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Power", + key="charge_power", + register=0x8, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + allowedtypes=X1 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Power L1", + key="charge_power_l1", + register=0x8, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + allowedtypes=X3 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Power L2", + key="charge_power_l2", + register=0x9, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + allowedtypes=X3 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Power L3", + key="charge_power_l3", + register=0xA, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + allowedtypes=X3 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Power Total", + key="charge_power_total", + register=0xB, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + allowedtypes=G1 | G2, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Time", + key="charge_time", + register=0x2B, + native_unit_of_measurement=UnitOfTime.SECONDS, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL, + entity_registry_enabled_default=True, + allowedtypes=G1, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Frequency", + key="charge_frequency", + register=0xC, + scale=0.01, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + allowedtypes=X1 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Frequency L1", + key="charge_frequency_l1", + register=0xC, + scale=0.01, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + allowedtypes=X3 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Frequency L2", + key="charge_frequency_l2", + register=0xD, + scale=0.01, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + allowedtypes=X3 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Frequency L3", + key="charge_frequency_l3", + register=0xE, + scale=0.01, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + allowedtypes=X3 | G1 | G2, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Added", + key="charge_added", + register=0xF, + scale=0.1, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + allowedtypes=G1, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charge Added Total", + key="charge_added_total", + register=0x10, + unit=U32, + scale=0.1, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, + allowedtypes=G1 | G2, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Grid Current", + key="grid_current", + register=0x12, + unit=S16, + scale=0.01, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + allowedtypes=X1 | G1, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Grid Current L1", + key="grid_current_l1", + register=0x12, + unit=S16, + scale=0.01, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + allowedtypes=X3 | G1, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Grid Current L2", + key="grid_current_l2", + register=0x13, + unit=S16, + scale=0.01, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + allowedtypes=X3 | G1, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Grid Current L3", + key="grid_current_l3", + register=0x14, + unit=S16, + scale=0.01, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + allowedtypes=X3 | G1, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Grid Power", + key="grid_power", + register=0x15, + unit=S16, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + allowedtypes=X1 | G1, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Grid Power L1", + key="grid_power_l1", + register=0x15, + unit=S16, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + allowedtypes=X3 | G1, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Grid Power L2", + key="grid_power_l2", + register=0x16, + unit=S16, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + allowedtypes=X3 | G1, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Grid Power L3", + key="grid_power_l3", + register=0x17, + unit=S16, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + allowedtypes=X3 | G1, + entity_registry_enabled_default=False, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Available PV Power", + key="available_pv_power", + register=0x18, + unit=S16, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + allowedtypes=G1, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Charger Temperature", + key="charger_temperature", + register=0x1C, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + allowedtypes=G1, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Run Mode", + key="run_mode", + unit=U16, + register=0x1D, + scale={ + 0: "Available", + 1: "Preparing", + 2: "Charging", + 3: "Finishing", + 4: "Fault Mode", + 5: "Unavailable", + 6: "Reserved", + 7: "Suspended EV", + 8: "Suspended EVSE", + 9: "Update", + 10: "RFID Activation", + }, + icon="mdi:run", + allowedtypes=G1 | G2, + ), + SolaXEVChargerHttpSensorEntityDescription( + name="Firmware Version", + key="firmwareversion", + register=0x25, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information", + allowedtypes=G1, + ), +] diff --git a/custom_components/solax_http/manifest.json b/custom_components/solax_http/manifest.json index 8f23e7d..bd4b080 100644 --- a/custom_components/solax_http/manifest.json +++ b/custom_components/solax_http/manifest.json @@ -10,5 +10,5 @@ "name": "SolaX HTTP API", "requirements": [], "issue_tracker": "https://github.com/PatrikTrestik/homeassistant-solax-http/issues", - "version": "1.2.8" + "version": "1.3.1" } diff --git a/custom_components/solax_http/number.py b/custom_components/solax_http/number.py index deb5d7f..6181dd8 100644 --- a/custom_components/solax_http/number.py +++ b/custom_components/solax_http/number.py @@ -4,17 +4,18 @@ from homeassistant.core import HomeAssistant, callback from .const import ATTR_MANUFACTURER, DOMAIN, BaseHttpNumberEntityDescription from .const import BaseHttpSelectEntityDescription -from .const import plugin_base +from .plugin_base import plugin_base from .coordinator import SolaxHttpUpdateCoordinator from typing import Any, Dict, Optional import logging _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities) -> None: + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities) -> None: name = entry.options[CONF_NAME] coordinator: SolaxHttpUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - plugin:plugin_base=coordinator.plugin + plugin: plugin_base = coordinator.plugin device_info = { "identifiers": {(DOMAIN, name)}, @@ -24,35 +25,31 @@ async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities) -> No entities = [] for number_info in plugin.NUMBER_TYPES: - if plugin.matchWithMask(number_info.allowedtypes,number_info.blacklist): - select = SolaXHttpNumber( - coordinator, - name, - device_info, - number_info - ) + if plugin.matchWithMask(number_info.allowedtypes, number_info.blacklist): + select = SolaXHttpNumber(coordinator, name, device_info, number_info) entities.append(select) async_add_entities(entities) return True + class SolaXHttpNumber(CoordinatorEntity, NumberEntity): """Representation of an SolaX Http number.""" def __init__( - self, - coordinator:SolaxHttpUpdateCoordinator, - platform_name, - device_info, - description: BaseHttpNumberEntityDescription, + self, + coordinator: SolaxHttpUpdateCoordinator, + platform_name, + device_info, + description: BaseHttpNumberEntityDescription, ) -> None: """Initialize the number.""" super().__init__(coordinator, context=description) self._platform_name = platform_name self._attr_device_info = device_info self.entity_description = description - self._value=None + self._value = None async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -73,7 +70,6 @@ def name(self): """Return the name.""" return f"{self._platform_name} {self.entity_description.name}" - @property def unique_id(self) -> Optional[str]: return f"{self._platform_name}_{self.entity_description.key}" @@ -82,9 +78,10 @@ def unique_id(self) -> Optional[str]: def native_value(self) -> float: return self._value - async def async_set_native_value(self, value: float) -> None: """Change the number value.""" payload = value - success=await self.coordinator.write_register(self.entity_description.register, payload) + success = await self.coordinator.write_register( + self.entity_description.register, payload + ) await self.coordinator.async_request_refresh() diff --git a/custom_components/solax_http/plugin_base.py b/custom_components/solax_http/plugin_base.py new file mode 100644 index 0000000..19f480f --- /dev/null +++ b/custom_components/solax_http/plugin_base.py @@ -0,0 +1,55 @@ +"""Module provides the base plugin class for Solax HTTP integration.""" + +from dataclasses import dataclass +import logging + +from homeassistant.components.button import ButtonEntityDescription +from homeassistant.components.number import NumberEntityDescription +from homeassistant.components.select import SelectEntityDescription +from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.time import TimeEntityDescription + +from .entity_definitions import ALL_POW_GROUP, ALL_VER_GROUP, ALL_X_GROUP + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class plugin_base: + plugin_name: str + TIME_TYPES: list[TimeEntityDescription] + SENSOR_TYPES: list[SensorEntityDescription] + BUTTON_TYPES: list[ButtonEntityDescription] + NUMBER_TYPES: list[NumberEntityDescription] + SELECT_TYPES: list[SelectEntityDescription] + + invertertype = None + + async def initialize(self) -> None: + pass + + def map_data(self, descr, data) -> any: + return None + + def map_payload(self, address, payload): + return None + + def matchWithMask(self, entitymask, blacklist=None): + if self.invertertype is None or self.invertertype == 0: + return False + # returns true if the entity needs to be created for an inverter + powmatch = ((self.invertertype & entitymask & ALL_POW_GROUP) != 0) or ( + entitymask & ALL_POW_GROUP == 0 + ) + xmatch = ((self.invertertype & entitymask & ALL_X_GROUP) != 0) or ( + entitymask & ALL_X_GROUP == 0 + ) + vermatch = ((self.invertertype & entitymask & ALL_VER_GROUP) != 0) or ( + entitymask & ALL_VER_GROUP == 0 + ) + blacklisted = False + if blacklist: + for start in blacklist: + if self._serialnumber.startswith(start): + blacklisted = True + return (powmatch and xmatch and vermatch) and not blacklisted diff --git a/custom_components/solax_http/plugin_factory.py b/custom_components/solax_http/plugin_factory.py new file mode 100644 index 0000000..b249cbe --- /dev/null +++ b/custom_components/solax_http/plugin_factory.py @@ -0,0 +1,146 @@ +"""Module contains the PluginFactory class which is used to create instances of plugins.""" + +import json +import logging + +import aiohttp + +from .entity_definitions import ( + BUTTON_TYPES, + NUMBER_TYPES, + POW7, + POW11, + POW22, + SELECT_TYPES, + SENSOR_TYPES, + TIME_TYPES, + V10, + V11, + V20, + X1, + X3, +) +from .plugin_solax_ev_charger import solax_ev_charger_plugin +from .plugin_solax_ev_charger_g2 import solax_ev_charger_plugin_g2 + +_LOGGER = logging.getLogger(__name__) + + +class PluginFactory: + """Factory class to create plugin instances.""" + + @staticmethod + async def _http_post(url, payload, retry=3): + try: + connector = aiohttp.TCPConnector( + force_close=True, + ) + async with ( + aiohttp.ClientSession(connector=connector) as session, + session.post(url, data=payload) as resp, + ): + if resp.status == 200: + return await resp.text() + except TimeoutError: + if retry > 0: + return await PluginFactory._http_post(url, payload, retry - 1) + except aiohttp.ServerDisconnectedError: + if retry: + return await PluginFactory._http_post(url, payload, retry - 1) + except aiohttp.client_exceptions.ClientOSError: + if retry > 0: + return await PluginFactory._http_post(url, payload, retry - 1) + except Exception as ex: + _LOGGER.exception("Error reading from Http. Url: %s", url, exc_info=ex) + return None + + @staticmethod + async def _read_serial_number(host: str, pwd: str): + httpData = None + text = await PluginFactory._http_post( + f"http://{host}", f"optType=ReadRealTimeData&pwd={pwd}" + ) + if text is None: + return None + if "failed" in text: + _LOGGER.error("Failed to read data from http: %s", text) + return None + try: + httpData = json.loads(text) + except json.decoder.JSONDecodeError: + _LOGGER.error("Failed to decode json: %s", text) + return httpData["Information"][2] + + @staticmethod + def _determine_type(sn: str): + _LOGGER.info("Trying to determine inverter type") + invertertype = 0 + # derive invertertupe from seriiesnumber + # Adding support for G2 HEC + if sn.startswith("C"): # G1 EVC + # Version + if sn[4] == "0": + invertertype = invertertype | V10 + elif sn[4] == "1": + invertertype = invertertype | V11 + # Phases + if sn[1] == "1": + invertertype = invertertype | X1 + elif sn[1] == "3": + invertertype = invertertype | X3 + # Power + if sn[2:4] == "07": + invertertype = invertertype | POW7 + elif sn[2:4] == "11": + invertertype = invertertype | POW11 + elif sn[2:4] == "22": + invertertype = invertertype | POW22 + elif sn.startswith("50"): # G2 HEC + # Version + invertertype = V20 + # Phases + if sn[2] == "3": + invertertype = invertertype | X3 + # Power + if sn[4] == "B": + invertertype = invertertype | POW11 + elif sn[4] == "M": + invertertype = invertertype | POW22 + else: + _LOGGER.error("Unrecognized inverter type - serial number: %s", sn) + return None + return invertertype + + @staticmethod + async def get_plugin_instance(host: str, pwd: str): + """Get an instance of plugin based on serial number/type.""" + sn = await PluginFactory._read_serial_number(host, pwd) + if sn is None: + _LOGGER.warning("Attempt to read serialnumber failed") + return None + _LOGGER.info("Read serial number: %s", sn) + invertertype = PluginFactory._determine_type(sn) + if invertertype: + if invertertype & V10 or invertertype & V11: + return solax_ev_charger_plugin( + serialnumber=sn, + invertertype=invertertype, + plugin_name="solax_ev_charger", + TIME_TYPES=TIME_TYPES, + SENSOR_TYPES=SENSOR_TYPES, + NUMBER_TYPES=NUMBER_TYPES, + BUTTON_TYPES=BUTTON_TYPES, + SELECT_TYPES=SELECT_TYPES, + ) + if invertertype & V20: + return solax_ev_charger_plugin_g2( + serialnumber=sn, + invertertype=invertertype, + plugin_name="solax_ev_charger_g2", + TIME_TYPES=TIME_TYPES, + SENSOR_TYPES=SENSOR_TYPES, + NUMBER_TYPES=NUMBER_TYPES, + BUTTON_TYPES=BUTTON_TYPES, + SELECT_TYPES=SELECT_TYPES, + ) + raise ValueError(f"Unknown inverter type: {sn}") diff --git a/custom_components/solax_http/plugin_solax_ev_charger.py b/custom_components/solax_http/plugin_solax_ev_charger.py index fe9ca7a..478251d 100644 --- a/custom_components/solax_http/plugin_solax_ev_charger.py +++ b/custom_components/solax_http/plugin_solax_ev_charger.py @@ -1,900 +1,107 @@ -from ctypes import cast +from dataclasses import dataclass import datetime import logging -from dataclasses import dataclass -from .const import S16, U16, U32, BaseHttpSwitchEntityDescription, plugin_base -from homeassistant.components.number.const import NumberDeviceClass -from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass -from homeassistant.const import EntityCategory, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfFrequency, UnitOfPower, UnitOfTemperature, UnitOfTime -from .const import BaseHttpButtonEntityDescription, BaseHttpNumberEntityDescription, BaseHttpSelectEntityDescription, BaseHttpSensorEntityDescription, BaseHttpTimeEntityDescription +from .entity_definitions import ALL_POW_GROUP, ALL_VER_GROUP, ALL_X_GROUP, S16 +from .plugin_base import plugin_base _LOGGER = logging.getLogger(__name__) -""" ============================================================================================ -bitmasks definitions to characterize inverters, ogranized by group -these bitmasks are used in entitydeclarations to determine to which inverters the entity applies -within a group, the bits in an entitydeclaration will be interpreted as OR -between groups, an AND condition is applied, so all gruoups must match. -An empty group (group without active flags) evaluates to True. -example: GEN3 | GEN4 | X1 | X3 | EPS -means: any inverter of tyoe (GEN3 or GEN4) and (X1 or X3) and (EPS) -An entity can be declared multiple times (with different bitmasks) if the parameters are different for each inverter type -""" - -POW7 = 0x0001 -POW11 = 0x0002 -POW22 = 0x0004 -ALL_POW_GROUP = POW7 | POW11 | POW22 - -X1 = 0x0100 -X3 = 0x0200 -ALL_X_GROUP = X1 | X3 - -V10 = 0x0010 -V11 = 0x0020 -ALL_VER_GROUP = V10 | V11 - -ALLDEFAULT = 0 - -# ======================= end of bitmask handling code ============================================= - -SENSOR_TYPES = [] -# ================================================================================================= - -@dataclass -class SolaXEVChargerHttpButtonEntityDescription(BaseHttpButtonEntityDescription): - allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice - -@dataclass -class SolaXEVChargerHttpSwitchEntityDescription(BaseHttpSwitchEntityDescription): - allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice - -@dataclass -class SolaXEVChargerHttpNumberEntityDescription(BaseHttpNumberEntityDescription): - allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice - -@dataclass -class SolaXEVChargerHttpTimeEntityDescription(BaseHttpNumberEntityDescription): - allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice - -@dataclass -class SolaXEVChargerHttpSelectEntityDescription(BaseHttpSelectEntityDescription): - allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice - -@dataclass -class SolaXEVChargerHttpSensorEntityDescription(BaseHttpSensorEntityDescription): - allowedtypes: int = ALLDEFAULT # maybe 0x0000 (nothing) is a better default choice - -# ====================================== Computed value functions ================================================= - -# ================================= Button Declarations ============================================================ - -BUTTON_TYPES = [ - SolaXEVChargerHttpButtonEntityDescription( - name = "Reset", - key = "reset", - register = 0x618, - icon = "mdi:reset" - ) - # SolaXEVChargerHttpButtonEntityDescription( - # name = "Sync RTC", - # key = "sync_rtc", - # register = 0x61E, - # icon = "mdi:home-clock", - # value_function = value_function_sync_rtc, - # ), -] - -# ================================= Switch Declarations ============================================================ - -SWITCH_TYPES = [ - # SolaXEVChargerHttpSwitchEntityDescription( - # name = "Boost set", - # key = "boost_set", - # register = 0x61E, - # icon = "mdi:home-clock", - # value_function = value_function_sync_rtc, - # ), -] -# ================================= Time Declarations ============================================================ - -TIME_TYPES = [ - ### - # - # Normal time types - # - ### - SolaXEVChargerHttpTimeEntityDescription( - name = "Timed boost start", - key = "timed_boost_start", - register = 0x634, - ), - SolaXEVChargerHttpTimeEntityDescription( - name = "Timed boost end", - key = "timed_boost_end", - register = 0x636, - ), - SolaXEVChargerHttpTimeEntityDescription( - name = "Smart boost end", - key = "smart_boost_end", - register = 0x638, - ), -] - -# ================================= Number Declarations ============================================================ - -NUMBER_TYPES = [ - ### - # - # Data only number types - # - ### - - ### - # - # Normal number types - # - ### - # SolaXEVChargerHttpNumberEntityDescription( - # name = "Datahub Charge Current", - # key = "datahub_charge_current", - # register = 0x624, - # fmt = "f", - # native_min_value = 6, - # native_max_value = 32, - # native_step = 0.1, - # scale = 0.01, - # native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - # device_class = NumberDeviceClass.CURRENT, - # ), - SolaXEVChargerHttpNumberEntityDescription( - name = "Max Charge Current Setting", - key = "max_charge_current_setting", - register = 0x628, - fmt = "f", - native_min_value = 6, - native_max_value = 16, - native_step = 1, - scale = 1, - allowedtypes = POW11, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = NumberDeviceClass.CURRENT, - ), - SolaXEVChargerHttpNumberEntityDescription( - name = "Max Charge Current Setting", - key = "max_charge_current_setting", - register = 0x628, - fmt = "f", - native_min_value = 6, - native_max_value = 32, - native_step = 1, - scale = 1, - allowedtypes = POW7 | POW22, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = NumberDeviceClass.CURRENT, - ), - SolaXEVChargerHttpNumberEntityDescription( - name = "Smart boost energy", - key = "smart_boost_energy", - register = 0x63A, - fmt = "f", - native_min_value = 0, - native_max_value = 100, - native_step = 1, - scale = 1, - native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR, - device_class = NumberDeviceClass.ENERGY, - ), -] - -# ================================= Select Declarations ============================================================ - -SELECT_TYPES = [ - ### - # - # Normal select types - # - ### - SolaXEVChargerHttpSelectEntityDescription( - name = "Meter Setting", - key = "meter_setting", - register = 0x60C, - scale = { - 0: "External CT", - 1: "External Meter", - 2: "Inverter", }, - entity_registry_enabled_default = False, - entity_category = EntityCategory.DIAGNOSTIC, - icon = "mdi:meter-electric", - ), - SolaXEVChargerHttpSelectEntityDescription( - name = "Charger Use Mode", - key = "charger_use_mode", - register = 0x60D, - scale = { - 0: "Stop", - 1: "Fast", - 2: "ECO", - 3: "Green", }, - icon = "mdi:dip-switch", - ), - SolaXEVChargerHttpSelectEntityDescription( - name = "Charger Green Mode Level", - key = "charger_green_mode", - register = 0x60F, - scale = { - 3: "3A", - 6: "6A", - }, - icon = "mdi:dip-switch", - ), - SolaXEVChargerHttpSelectEntityDescription( - name = "Charger Eco Mode Level", - key = "charger_eco_mode", - register = 0x60E, - scale = { - 6: "6A", - 10: "10A", - 16: "16A", - 20: "20A", - 25: "25A", - }, - icon = "mdi:dip-switch", - allowedtypes = POW7 | POW22, - ), - SolaXEVChargerHttpSelectEntityDescription( - name = "Charger Eco Mode Level", - key = "charger_eco_mode", - register = 0x60E, - scale = { - 6: "6A", - 10: "10A" - }, - icon = "mdi:dip-switch", - allowedtypes = POW11, - ), - SolaXEVChargerHttpSelectEntityDescription( - name = "Start Charge Mode", - key = "start_charge_mode", - register = 0x610, - scale = { - 0: "Plug & Charge", - 1: "RFID to Charge", }, - icon = "mdi:lock", - ), - SolaXEVChargerHttpSelectEntityDescription( - name = "Boost Mode", - key = "boost_mode", - register = 0x613, - scale = { - 0: "Normal", - 1: "Timer Boost", - 2: "Smart Boost", }, - icon = "mdi:dip-switch", - ), - # SolaXEVChargerHttpSelectEntityDescription( - # name = "Device Lock", - # key = "device_lock", - # register = 0x615, - # option_dict = { - # 0: "Unlock", - # 1: "Lock", }, - # icon = "mdi:lock", - # ), - # SolaXEVChargerHttpSelectEntityDescription( - # name = "RFID Program", - # key = "rfid_program", - # register = 0x616, - # option_dict = { - # 0: "Program New", - # 1: "Program Off", }, - # icon = "mdi:dip-switch", - # ), - SolaXEVChargerHttpSelectEntityDescription( - name = "Charge Phase", - key = "charge_phase", - register = 0x625, - scale = { - 0: "Three Phase", - 1: "L1 Phase", - 2: "L2 Phase", - 3: "L3 Phase", }, - icon = "mdi:dip-switch", - ), - # SolaXEVChargerHttpSelectEntityDescription( - # name = "Control Command", - # key = "control_command", - # register = 0x627, - # option_dict = { - # 1: "Available", - # 2: "Unavailable", - # 3: "Stop charging", - # 4: "Start Charging", - # 5: "Reserve", - # 6: "Cancel the Reservation", }, - # icon = "mdi:dip-switch", - # ), -] - -# ================================= Sennsor Declarations ============================================================ - -SENSOR_TYPES_MAIN: list[SolaXEVChargerHttpSensorEntityDescription] = [ - ### - # - # Holding - # - ### - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge start time", - key = "charge_start_time", - register = 0xF001, - device_class = SensorDeviceClass.TIMESTAMP, - entity_registry_enabled_default = True, - icon = "mdi:clock", - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "CT Meter Setting", - key = "ct_meter_setting", - register = 0x60C, - scale = { - 0: "External CT", - 1: "External Meter", - 2: "Inverter", }, - entity_registry_enabled_default = False, - entity_category = EntityCategory.DIAGNOSTIC, - icon = "mdi:meter-electric", - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charger Use Mode", - key = "charger_use_mode", - register = 0x60D, - scale = { - 0: "Stop", - 1: "Fast", - 2: "ECO", - 3: "Green", }, - entity_registry_enabled_default = False, - icon = "mdi:dip-switch", - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Start Charge Mode", - key = "start_charge_mode", - register = 0x610, - scale = { - 0: "Plug & Charge", - 1: "RFID to Charge", }, - entity_registry_enabled_default = False, - icon = "mdi:lock", - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Boost Mode", - key = "boost_mode", - register = 0x613, - scale = { - 0: "Normal", - 1: "Timer Boost", - 2: "Smart Boost", }, - entity_registry_enabled_default = False, - icon = "mdi:dip-switch", - ), - # SolaXEVChargerHttpSensorEntityDescription( - # name = "Device Lock", - # key = "device_lock", - # register = 0x615, - # scale = { - # 0: "Unlock", - # 1: "Lock", }, - # entity_registry_enabled_default = False, - # icon = "mdi:lock", - # ), - # SolaXEVChargerHttpSensorEntityDescription( - # name = "RFID Program", - # key = "rfid_program", - # register = 0x616, - # scale = { - # 0: "Program New", - # 1: "Program Off", }, - # entity_registry_enabled_default = False, - # icon = "mdi:dip-switch", - # ), - # SolaXEVChargerHttpSensorEntityDescription( - # name = "RTC", - # key = "rtc", - # register = 0x61E, - # unit = WORDS, - # wordcount = 6, - # scale = value_function_rtc, - # entity_registry_enabled_default = False, - # entity_category = EntityCategory.DIAGNOSTIC, - # icon = "mdi:clock", - # ), - # SolaXEVChargerHttpSensorEntityDescription( - # name = "Datahub Charge Current", - # key = "datahub_charge_current", - # register = 0x624, - # scale = 0.01, - # rounding = 1, - # native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - # device_class = SensorDeviceClass.CURRENT, - # allowedtypes = HYBRID, - # entity_registry_enabled_default = False, - # ), - # SolaXEVChargerHttpSensorEntityDescription( - # name = "Charge Phase", - # key = "charge_phase", - # register = 0x625, - # scale = { - # 0: "Three Phase", - # 1: "L1 Phase", - # 2: "L2 Phase", - # 3: "L3 Phase", }, - # allowedtypes = X3, - # entity_registry_enabled_default = False, - # icon = "mdi:dip-switch", - # ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Max Charge Current", - key = "max_charge_current", - register = 0x628, - scale = 1, - rounding = 0, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = SensorDeviceClass.CURRENT, - entity_registry_enabled_default = False, - ), - # SolaXEVChargerHttpSensorEntityDescription( - # name = "Control Command", - # key = "control_command", - # register = 0x627, - # scale = { - # 1: "Available", - # 2: "Unavailable", - # 3: "Stop charging", - # 4: "Start Charging", - # 5: "Reserve", - # 6: "Cancel the Reservation", }, - # entity_registry_enabled_default = False, - # icon = "mdi:dip-switch", - # ), - ### - # - # Input - # - ### - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Voltage", - key = "charge_voltage", - register = 0x0, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricPotential.VOLT, - device_class = SensorDeviceClass.VOLTAGE, - allowedtypes = X1, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Voltage L1", - key = "charge_voltage_l1", - register = 0x0, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricPotential.VOLT, - device_class = SensorDeviceClass.VOLTAGE, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Voltage L2", - key = "charge_voltage_l2", - register = 0x1, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricPotential.VOLT, - device_class = SensorDeviceClass.VOLTAGE, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Voltage L3", - key = "charge_voltage_l3", - register = 0x2, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricPotential.VOLT, - device_class = SensorDeviceClass.VOLTAGE, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - # SolaXEVChargerHttpSensorEntityDescription( - # name = "Charge PE Voltage", - # key = "charge_pe_voltage", - # register = 0x3, - # scale = 0.01, - # native_unit_of_measurement = UnitOfElectricPotential.VOLT, - # device_class = SensorDeviceClass.VOLTAGE, - # entity_registry_enabled_default = False, - # ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Current", - key = "charge_current", - register = 0x4, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = SensorDeviceClass.CURRENT, - allowedtypes = X1, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Current L1", - key = "charge_current_l1", - register = 0x4, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = SensorDeviceClass.CURRENT, - allowedtypes = X3, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Current L2", - key = "charge_current_l2", - register = 0x5, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = SensorDeviceClass.CURRENT, - allowedtypes = X3, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Current L3", - key = "charge_current_l3", - register = 0x6, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = SensorDeviceClass.CURRENT, - allowedtypes = X3, - ), - # SolaXEVChargerHttpSensorEntityDescription( - # name = "Charge PE Current", - # key = "charge_pe_current", - # register = 0x7, - # register_type = REG_INPUT, - # native_unit_of_measurement = UnitOfElectricCurrent.MILLIAMPERE, - # device_class = SensorDeviceClass.CURRENT, - # entity_registry_enabled_default = False, - # ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Power", - key = "charge_power", - register = 0x8, - native_unit_of_measurement = UnitOfPower.WATT, - device_class = SensorDeviceClass.POWER, - state_class = SensorStateClass.MEASUREMENT, - allowedtypes = X1, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Power L1", - key = "charge_power_l1", - register = 0x8, - native_unit_of_measurement = UnitOfPower.WATT, - device_class = SensorDeviceClass.POWER, - state_class = SensorStateClass.MEASUREMENT, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Power L2", - key = "charge_power_l2", - register = 0x9, - native_unit_of_measurement = UnitOfPower.WATT, - device_class = SensorDeviceClass.POWER, - state_class = SensorStateClass.MEASUREMENT, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Power L3", - key = "charge_power_l3", - register = 0xA, - native_unit_of_measurement = UnitOfPower.WATT, - device_class = SensorDeviceClass.POWER, - state_class = SensorStateClass.MEASUREMENT, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Power Total", - key = "charge_power_total", - register = 0xB, - native_unit_of_measurement = UnitOfPower.WATT, - device_class = SensorDeviceClass.POWER, - state_class = SensorStateClass.MEASUREMENT, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Time", - key = "charge_time", - register = 0x2B, - native_unit_of_measurement = UnitOfTime.SECONDS, - device_class = SensorDeviceClass.DURATION, - state_class = SensorStateClass.TOTAL, - entity_registry_enabled_default = True, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Frequency", - key = "charge_frequency", - register = 0xC, - scale = 0.01, - native_unit_of_measurement = UnitOfFrequency.HERTZ, - allowedtypes = X1, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Frequency L1", - key = "charge_frequency_l1", - register = 0xC, - scale = 0.01, - native_unit_of_measurement = UnitOfFrequency.HERTZ, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Frequency L2", - key = "charge_frequency_l2", - register = 0xD, - scale = 0.01, - native_unit_of_measurement = UnitOfFrequency.HERTZ, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Frequency L3", - key = "charge_frequency_l3", - register = 0xE, - scale = 0.01, - native_unit_of_measurement = UnitOfFrequency.HERTZ, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Added", - key = "charge_added", - register = 0xF, - scale = 0.1, - native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR, - device_class = SensorDeviceClass.ENERGY, - state_class = SensorStateClass.TOTAL_INCREASING, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charge Added Total", - key = "charge_added_total", - register = 0x10, - unit = U32, - scale = 0.1, - native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR, - device_class = SensorDeviceClass.ENERGY, - state_class = SensorStateClass.TOTAL_INCREASING, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Grid Current", - key = "grid_current", - register = 0x12, - unit = S16, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = SensorDeviceClass.CURRENT, - allowedtypes = X1, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Grid Current L1", - key = "grid_current_l1", - register = 0x12, - unit = S16, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = SensorDeviceClass.CURRENT, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Grid Current L2", - key = "grid_current_l2", - register = 0x13, - unit = S16, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = SensorDeviceClass.CURRENT, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Grid Current L3", - key = "grid_current_l3", - register = 0x14, - unit = S16, - scale = 0.01, - native_unit_of_measurement = UnitOfElectricCurrent.AMPERE, - device_class = SensorDeviceClass.CURRENT, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Grid Power", - key = "grid_power", - register = 0x15, - unit = S16, - native_unit_of_measurement = UnitOfPower.WATT, - device_class = SensorDeviceClass.POWER, - state_class = SensorStateClass.MEASUREMENT, - allowedtypes = X1, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Grid Power L1", - key = "grid_power_l1", - register = 0x15, - unit = S16, - native_unit_of_measurement = UnitOfPower.WATT, - device_class = SensorDeviceClass.POWER, - state_class = SensorStateClass.MEASUREMENT, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Grid Power L2", - key = "grid_power_l2", - register = 0x16, - unit = S16, - native_unit_of_measurement = UnitOfPower.WATT, - device_class = SensorDeviceClass.POWER, - state_class = SensorStateClass.MEASUREMENT, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Grid Power L3", - key = "grid_power_l3", - register = 0x17, - unit = S16, - native_unit_of_measurement = UnitOfPower.WATT, - device_class = SensorDeviceClass.POWER, - state_class = SensorStateClass.MEASUREMENT, - allowedtypes = X3, - entity_registry_enabled_default = False, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Available PV Power", - key = "available_pv_power", - register = 0x18, - unit = S16, - native_unit_of_measurement = UnitOfPower.WATT, - device_class = SensorDeviceClass.POWER, - state_class = SensorStateClass.MEASUREMENT, - entity_registry_enabled_default = True, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Charger Temperature", - key = "charger_temperature", - register = 0x1C, - native_unit_of_measurement = UnitOfTemperature.CELSIUS, - device_class = SensorDeviceClass.TEMPERATURE, - state_class = SensorStateClass.MEASUREMENT, - entity_category = EntityCategory.DIAGNOSTIC, - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Run Mode", - key = "run_mode", - unit = U16, - register = 0x1D, - scale = { 0: "Available", - 1: "Preparing", - 2: "Charging", - 3: "Finishing", - 4: "Fault Mode", - 5: "Unavailable", - 6: "Reserved", - 7: "Suspended EV", - 8: "Suspended EVSE", - 9: "Update", - 10: "RFID Activation", }, - icon = "mdi:run", - ), - SolaXEVChargerHttpSensorEntityDescription( - name = "Firmware Version", - key = "firmwareversion", - register = 0x25, - entity_category = EntityCategory.DIAGNOSTIC, - icon = "mdi:information", - ), -] # ============================ plugin declaration ================================================= + @dataclass class solax_ev_charger_plugin(plugin_base): - _serialnumber:str=None - invertertype=None + """Plugin for SolaX EV Charger integration.""" - async def initialize(self, data): - self._serialnumber=await self._read_serialnr(data) - self.invertertype=await self._determine_type() + serialnumber: str = None + invertertype: int = None def map_payload(self, address, payload): + """Map the payload to the corresponding register based on the address.""" + match address: case 0x60D: - return [{"reg":2,"val":f"{payload}"}] + return [{"reg": 2, "val": f"{payload}"}] case 0x60C: - return [{"reg":1,"val":f"{payload}"}] + return [{"reg": 1, "val": f"{payload}"}] case 0x60E: - return [{"reg":3,"val":f"{payload}"}] + return [{"reg": 3, "val": f"{payload}"}] case 0x60F: - return [{"reg":4,"val":f"{payload}"}] + return [{"reg": 4, "val": f"{payload}"}] case 0x610: - return [{"reg":5,"val":f"{payload}"}] + return [{"reg": 5, "val": f"{payload}"}] case 0x613: - return [{"reg":11,"val":f"{payload}"}] + return [{"reg": 11, "val": f"{payload}"}] case 0x618: - return [{"reg":22,"val":f"{payload}"}] + return [{"reg": 22, "val": f"{payload}"}] case 0x625: - return [{"reg":70,"val":f"{payload}"}] + return [{"reg": 70, "val": f"{payload}"}] case 0x628: - return [{"reg":82,"val":f"{payload}"}] + return [{"reg": 82, "val": f"{payload}"}] case 0x634: if isinstance(payload, datetime.time): - time_val:datetime.time=payload - hour=time_val.hour - minute=time_val.minute - hm_payload=(hour << 8) + minute - return [{"reg":12,"val":f"{hm_payload}"}] + time_val: datetime.time = payload + hour = time_val.hour + minute = time_val.minute + hm_payload = (hour << 8) + minute + return [{"reg": 12, "val": f"{hm_payload}"}] return None case 0x636: if isinstance(payload, datetime.time): - time_val:datetime.time=payload - hour=time_val.hour - minute=time_val.minute - hm_payload=(hour << 8) + minute - return [{"reg":13,"val":f"{hm_payload}"}] + time_val: datetime.time = payload + hour = time_val.hour + minute = time_val.minute + hm_payload = (hour << 8) + minute + return [{"reg": 13, "val": f"{hm_payload}"}] return None case 0x638: if isinstance(payload, datetime.time): - time_val:datetime.time=payload - hour=time_val.hour - minute=time_val.minute - hm_payload=(hour << 8) + minute - return [{"reg":15,"val":f"{hm_payload}"}] + time_val: datetime.time = payload + hour = time_val.hour + minute = time_val.minute + hm_payload = (hour << 8) + minute + return [{"reg": 15, "val": f"{hm_payload}"}] return None case 0x63A: - return [{"reg":14,"val":f"{payload}"}] + return [{"reg": 14, "val": f"{payload}"}] case _: return None - def map_data(self, descr, data)->any: - Set=data['Set'] - Data=data['Data'] - Info=data['Info'] + def map_data(self, descr, data) -> any: + Set = data["Set"] + Data = data["Data"] + Info = data["Info"] - return_value=None + return_value = None match descr.register: case 0xF001: - #Y-M-D H:m:S - year=Data.get(84) >> 8 - month=Data.get(84) & 0x00FF - day=Data.get(83) >> 8 - hour=Data.get(83) & 0x00FF - minute=Data.get(82) >> 8 - second=Data.get(82) & 0x00FF - if month!=0: - return_value=datetime.datetime(2000+year,month,day,hour,minute,second).astimezone() + # Y-M-D H:m:S + year = Data.get(84) >> 8 + month = Data.get(84) & 0x00FF + day = Data.get(83) >> 8 + hour = Data.get(83) & 0x00FF + minute = Data.get(82) >> 8 + second = Data.get(82) & 0x00FF + if month != 0: + return_value = datetime.datetime( + 2000 + year, month, day, hour, minute, second + ).astimezone() case 0x600: - return_value=Info.get(2) + return_value = Info.get(2) case 0x60C: - return_value=Set.get(0) + return_value = Set.get(0) case 0x60D: - return_value=Set.get(1) + return_value = Set.get(1) case 0x60E: - return_value=Set.get(2) + return_value = Set.get(2) case 0x60F: - return_value=Set.get(3) + return_value = Set.get(3) case 0x610: - return_value=Set.get(4) + return_value = Set.get(4) case 0x613: - return_value=Set.get(11) + return_value = Set.get(11) # case 0x615: # return_value=Data[] # case 0x616: @@ -902,162 +109,127 @@ def map_data(self, descr, data)->any: # case 0x61E: # RTC: Y-M-D H:m:S # return_value=[Data[38]H, Data[38]L, Data[37]H, Data[37]L, Data[36]H, Data[36]L] case 0x625: - return_value=Data.get(65) + return_value = Data.get(65) case 0x628: - return_value=Set.get(76) + return_value = Set.get(76) case 0x634: - val=Set.get(12) + val = Set.get(12) if val is not None: - hour=val >> 8 - minute=val & 0x00FF - if hour>=0 and hour<24 and minute>=0 and minute<60: - return_value=datetime.time(hour, minute) + hour = val >> 8 + minute = val & 0x00FF + if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: + return_value = datetime.time(hour, minute) case 0x636: - val=Set.get(13) + val = Set.get(13) if val is not None: - hour=val >> 8 - minute=val & 0x00FF - if hour>=0 and hour<24 and minute>=0 and minute<60: - return_value=datetime.time(hour, minute) + hour = val >> 8 + minute = val & 0x00FF + if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: + return_value = datetime.time(hour, minute) case 0x638: - val=Set.get(15) + val = Set.get(15) if val is not None: - hour=val >> 8 - minute=val & 0x00FF - if hour>=0 and hour<24 and minute>=0 and minute<60: - return_value=datetime.time(hour, minute) + hour = val >> 8 + minute = val & 0x00FF + if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: + return_value = datetime.time(hour, minute) case 0x63A: - return_value=Set.get(14) + return_value = Set.get(14) case 0x0: - return_value=Data.get(2) + return_value = Data.get(2) case 0x1: - return_value=Data.get(3) + return_value = Data.get(3) case 0x2: - return_value=Data.get(4) + return_value = Data.get(4) case 0x4: - return_value=Data.get(5) + return_value = Data.get(5) case 0x5: - return_value=Data.get(6) + return_value = Data.get(6) case 0x6: - return_value=Data.get(7) + return_value = Data.get(7) case 0x8: - return_value=Data.get(8) + return_value = Data.get(8) case 0x9: - return_value=Data.get(9) + return_value = Data.get(9) case 0xA: - return_value=Data.get(10) + return_value = Data.get(10) case 0xB: - return_value=Data.get(11) + return_value = Data.get(11) case 0xC: - return_value=Data.get(33) + return_value = Data.get(33) case 0xD: - return_value=Data.get(34) + return_value = Data.get(34) case 0xE: - return_value=Data.get(35) + return_value = Data.get(35) case 0xF: - return_value=Data.get(12) + return_value = Data.get(12) case 0x10: - datH=Data.get(15) - datL=Data.get(14) + datH = Data.get(15) + datL = Data.get(14) if datH is not None and datL is not None: - return_value=datH*65536+datL + return_value = datH * 65536 + datL case 0x12: - return_value=Data.get(16) + return_value = Data.get(16) case 0x13: - return_value=Data.get(17) + return_value = Data.get(17) case 0x14: - return_value=Data.get(18) + return_value = Data.get(18) case 0x15: - return_value=Data.get(19) + return_value = Data.get(19) case 0x16: - return_value=Data.get(20) + return_value = Data.get(20) case 0x17: - return_value=Data.get(21) + return_value = Data.get(21) case 0x18: - return_value=Data.get(22) + return_value = Data.get(22) case 0x1C: - return_value=Data.get(24) + return_value = Data.get(24) case 0x1D: - return_value=Data.get(0) + return_value = Data.get(0) case 0x25: - ver=str(Set.get(19)) + ver = str(Set.get(19)) if ver is not None: - return_value=f'{ver[0]}.{ver[1:]}' + return_value = f"{ver[0]}.{ver[1:]}" case 0x2B: - datH=Data.get(81) - datL=Data.get(80) + datH = Data.get(81) + datL = Data.get(80) if datH is not None and datL is not None: - return_value=datH*65536+datL+1 + return_value = datH * 65536 + datL + 1 case _: - return_value=None + return_value = None if return_value is None: return None if descr.unit == S16 and return_value >= 32768: - return_value=return_value - 65536 - if isinstance(descr.scale, dict): # translate int to string + return_value = return_value - 65536 + if isinstance(descr.scale, dict): # translate int to string return_value = descr.scale.get(return_value, "Unknown") elif callable(descr.scale): # function to call ? return_value = descr.scale(return_value, descr) - else: # apply simple numeric scaling and rounding if not a list of words + else: # apply simple numeric scaling and rounding if not a list of words try: - return_value = round(return_value*descr.scale, descr.rounding) + return_value = round(return_value * descr.scale, descr.rounding) except: - pass # probably a WORDS instance + pass # probably a WORDS instance return return_value - async def _read_serialnr(self, data): - res=data['Info'].get(2) - if res is None: - _LOGGER.warning(f"Attempt to read serialnumber failed") - return "unknown" - _LOGGER.info(f"Read serial number: {res}") - return res - - - async def _determine_type(self): - _LOGGER.info(f"Trying to determine inverter type") - - # derive invertertupe from seriiesnumber - # Adding support for G1.1 (C1071). If required add new flag to distinguish G1.0 (C1070) and G1.1 - if self._serialnumber.startswith('C107'): - invertertype = X1 | POW7 # 7kW EV Single Phase - elif self._serialnumber.startswith('C311'): - invertertype = X3 | POW11 # 11kW EV Three Phase - elif self._serialnumber.startswith('C322'): - invertertype = X3 | POW22 # 22kW EV Three Phase - # add cases here - else: - invertertype = 0 - _LOGGER.error(f"unrecognized inverter type - serial number : {self._serialnumber}") - - if self._serialnumber[4] == "0": - invertertype = invertertype | V10 - elif self._serialnumber[4] == "1": - invertertype = invertertype | V11 - return invertertype - - def matchWithMask(self, entitymask, blacklist = None): - if self.invertertype is None or self.invertertype==0: + def matchWithMask(self, entitymask, blacklist=None): + if self.invertertype is None or self.invertertype == 0: return False # returns true if the entity needs to be created for an inverter - powmatch = ((self.invertertype & entitymask & ALL_POW_GROUP) != 0) or (entitymask & ALL_POW_GROUP == 0) - xmatch = ((self.invertertype & entitymask & ALL_X_GROUP) != 0) or (entitymask & ALL_X_GROUP == 0) - vermatch = ((self.invertertype & entitymask & ALL_VER_GROUP) != 0) or (entitymask & ALL_VER_GROUP == 0) + powmatch = ((self.invertertype & entitymask & ALL_POW_GROUP) != 0) or ( + entitymask & ALL_POW_GROUP == 0 + ) + xmatch = ((self.invertertype & entitymask & ALL_X_GROUP) != 0) or ( + entitymask & ALL_X_GROUP == 0 + ) + vermatch = ((self.invertertype & entitymask & ALL_VER_GROUP) != 0) or ( + entitymask & ALL_VER_GROUP == 0 + ) blacklisted = False if blacklist: for start in blacklist: if self._serialnumber.startswith(start): blacklisted = True return (powmatch and xmatch and vermatch) and not blacklisted - -def get_plugin_instance(): - return solax_ev_charger_plugin( - plugin_name = 'solax_ev_charger', - TIME_TYPES=TIME_TYPES, - SENSOR_TYPES = SENSOR_TYPES_MAIN, - NUMBER_TYPES = NUMBER_TYPES, - BUTTON_TYPES = BUTTON_TYPES, - SELECT_TYPES = SELECT_TYPES, - ) diff --git a/custom_components/solax_http/plugin_solax_ev_charger_g2.py b/custom_components/solax_http/plugin_solax_ev_charger_g2.py new file mode 100644 index 0000000..c7c1133 --- /dev/null +++ b/custom_components/solax_http/plugin_solax_ev_charger_g2.py @@ -0,0 +1,217 @@ +from ctypes import cast +import datetime +import logging +from dataclasses import dataclass + +from .plugin_base import plugin_base +from .entity_definitions import ALL_POW_GROUP, ALL_X_GROUP, ALL_VER_GROUP, S16 + + +_LOGGER = logging.getLogger(__name__) + + +# ============================ plugin declaration ================================================= + + +@dataclass +class solax_ev_charger_plugin_g2(plugin_base): + """Plugin for SolaX EV Charger integration.""" + + serialnumber: str = None + invertertype: int = None + + def map_payload(self, address, payload): + """Map the payload to the corresponding register based on the address.""" + + match address: + case 0x60D: + return [{"reg": 2, "val": f"{payload}"}] + case 0x60C: + return [{"reg": 1, "val": f"{payload}"}] + case 0x60E: + return [{"reg": 3, "val": f"{payload}"}] + case 0x60F: + return [{"reg": 4, "val": f"{payload}"}] + case 0x610: + return [{"reg": 5, "val": f"{payload}"}] + case 0x613: + return [{"reg": 11, "val": f"{payload}"}] + case 0x618: + return [{"reg": 22, "val": f"{payload}"}] + case 0x625: + return [{"reg": 70, "val": f"{payload}"}] + case 0x628: + return [{"reg": 82, "val": f"{payload}"}] + case 0x634: + if isinstance(payload, datetime.time): + time_val: datetime.time = payload + hour = time_val.hour + minute = time_val.minute + hm_payload = (hour << 8) + minute + return [{"reg": 12, "val": f"{hm_payload}"}] + return None + case 0x636: + if isinstance(payload, datetime.time): + time_val: datetime.time = payload + hour = time_val.hour + minute = time_val.minute + hm_payload = (hour << 8) + minute + return [{"reg": 13, "val": f"{hm_payload}"}] + return None + case 0x638: + if isinstance(payload, datetime.time): + time_val: datetime.time = payload + hour = time_val.hour + minute = time_val.minute + hm_payload = (hour << 8) + minute + return [{"reg": 15, "val": f"{hm_payload}"}] + return None + case 0x63A: + return [{"reg": 14, "val": f"{payload}"}] + case _: + return None + + def map_data(self, descr, data) -> any: + Set = data["Set"] + Data = data["Data"] + Info = data["Info"] + + return_value = None + match descr.register: + # case 0xF001: #Min/Sec Last charge start + # # Y-M-D H:m:S + # year = Data.get(84) >> 8 + # month = Data.get(84) & 0x00FF + # day = Data.get(83) >> 8 + # hour = Data.get(83) & 0x00FF + # minute = Data.get(82) >> 8 + # second = Data.get(82) & 0x00FF + # if month != 0: + # return_value = datetime.datetime( + # 2000 + year, month, day, hour, minute, second + # ).astimezone() + case 0x600: + return_value = Info.get(2) + case 0x60C: + return_value = Set.get(0) + case 0x60D: + return_value = Set.get(1) + case 0x60E: + return_value = Set.get(2) + case 0x60F: + return_value = Set.get(3) + case 0x610: + return_value = Set.get(4) + case 0x613: + return_value = Set.get(11) + # case 0x615: + # return_value=Data[] + # case 0x616: + # return_value=Data[] + # case 0x61E: # RTC: Y-M-D H:m:S + # return_value=[Data[38]H, Data[38]L, Data[37]H, Data[37]L, Data[36]H, Data[36]L] + # case 0x625: + # return_value = Data.get(65) + case 0x628: + return_value = Set.get(76) + case 0x634: + val = Set.get(12) + if val is not None: + hour = val >> 8 + minute = val & 0x00FF + if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: + return_value = datetime.time(hour, minute) + case 0x636: + val = Set.get(13) + if val is not None: + hour = val >> 8 + minute = val & 0x00FF + if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: + return_value = datetime.time(hour, minute) + case 0x638: + val = Set.get(15) + if val is not None: + hour = val >> 8 + minute = val & 0x00FF + if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: + return_value = datetime.time(hour, minute) + case 0x63A: + return_value = Set.get(14) + case 0x0: + return_value = Data.get(3) + case 0x1: + return_value = Data.get(4) + case 0x2: + return_value = Data.get(5) + case 0x4: + return_value = Data.get(6) + case 0x5: + return_value = Data.get(7) + case 0x6: + return_value = Data.get(8) + case 0x8: + return_value = Data.get(9) + case 0x9: + return_value = Data.get(10) + case 0xA: + return_value = Data.get(11) + case 0xB: + return_value = Data.get(12) + case 0xC: + return_value = Data.get(33) + case 0xD: + return_value = Data.get(34) + case 0xE: + return_value = Data.get(35) + # case 0xF: #E Actual Charge + # return_value = Data.get(12) + case 0x10: + datH = Data.get(16) + datL = Data.get(15) + if datH is not None and datL is not None: + return_value = datH * 65536 + datL + # case 0x12: #EA1 + # return_value = Data.get(16) + # case 0x13: + # return_value = Data.get(17) + # case 0x14: + # return_value = Data.get(18) + # case 0x15: # EP1 + # return_value = Data.get(19) + # case 0x16: + # return_value = Data.get(20) + # case 0x17: + # return_value = Data.get(21) + # case 0x18: #Exces power + # return_value = Data.get(22) + # case 0x1C: #T PCB + # return_value = Data.get(24) + case 0x1D: + return_value = Data.get(0) + case 0x25: + ver = str(Set.get(19)) + if ver is not None: + return_value = f"{ver[0]}.{ver[1:]}" + # case 0x2B: #Charge time + # datH = Data.get(81) + # datL = Data.get(80) + # if datH is not None and datL is not None: + # return_value = datH * 65536 + datL + 1 + case _: + return_value = None + + if return_value is None: + return None + if descr.unit == S16 and return_value >= 32768: + return_value = return_value - 65536 + if isinstance(descr.scale, dict): # translate int to string + return_value = descr.scale.get(return_value, "Unknown") + elif callable(descr.scale): # function to call ? + return_value = descr.scale(return_value, descr) + else: # apply simple numeric scaling and rounding if not a list of words + try: + return_value = round(return_value * descr.scale, descr.rounding) + except: + pass # probably a WORDS instance + + return return_value diff --git a/custom_components/solax_http/select.py b/custom_components/solax_http/select.py index d5d3a73..30448b2 100644 --- a/custom_components/solax_http/select.py +++ b/custom_components/solax_http/select.py @@ -4,17 +4,18 @@ from homeassistant.core import HomeAssistant, callback from .const import ATTR_MANUFACTURER, DOMAIN from .const import BaseHttpSelectEntityDescription -from .const import plugin_base +from .plugin_base import plugin_base from .coordinator import SolaxHttpUpdateCoordinator from typing import Any, Dict, Optional import logging _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities) -> None: + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities) -> None: name = entry.options[CONF_NAME] coordinator: SolaxHttpUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - plugin:plugin_base=coordinator.plugin + plugin: plugin_base = coordinator.plugin device_info = { "identifiers": {(DOMAIN, name)}, @@ -24,29 +25,26 @@ async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities) -> No entities = [] for select_info in plugin.SELECT_TYPES: - if plugin.matchWithMask(select_info.allowedtypes,select_info.blacklist): - select = SolaXHttpSelect( - coordinator, - name, - device_info, - select_info - ) + if plugin.matchWithMask(select_info.allowedtypes, select_info.blacklist): + select = SolaXHttpSelect(coordinator, name, device_info, select_info) entities.append(select) async_add_entities(entities) return True + class SolaXHttpSelect(CoordinatorEntity, SelectEntity): """Representation of an SolaX Http select.""" + coordinator: SolaxHttpUpdateCoordinator def __init__( - self, - coordinator:SolaxHttpUpdateCoordinator, - platform_name, - device_info, - description: BaseHttpSelectEntityDescription, + self, + coordinator: SolaxHttpUpdateCoordinator, + platform_name, + device_info, + description: BaseHttpSelectEntityDescription, ) -> None: """Initialize the selector.""" super().__init__(coordinator, context=description) @@ -54,7 +52,7 @@ def __init__( self._attr_device_info = device_info self.entity_description = description self._attr_options = list(description.scale.values()) - self._value=None + self._value = None async def async_added_to_hass(self): """Register callbacks.""" @@ -92,5 +90,7 @@ def get_payload(self, my_dict, search): async def async_select_option(self, option: str) -> None: """Change the select option.""" payload = self.get_payload(self.entity_description.scale, option) - success=await self.coordinator.write_register(self.entity_description.register, payload) - await self.coordinator.async_request_refresh() \ No newline at end of file + success = await self.coordinator.write_register( + self.entity_description.register, payload + ) + await self.coordinator.async_request_refresh() diff --git a/custom_components/solax_http/sensor.py b/custom_components/solax_http/sensor.py index acabdb9..311a688 100644 --- a/custom_components/solax_http/sensor.py +++ b/custom_components/solax_http/sensor.py @@ -1,7 +1,7 @@ import numbers from homeassistant.helpers.update_coordinator import CoordinatorEntity from .coordinator import SolaxHttpUpdateCoordinator -from .const import plugin_base +from .plugin_base import plugin_base from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.components.sensor import SensorEntity @@ -15,10 +15,11 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities): + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): name = entry.options[CONF_NAME] coordinator: SolaxHttpUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - plugin:plugin_base=coordinator.plugin + plugin: plugin_base = coordinator.plugin device_info = { "identifiers": {(DOMAIN, name)}, @@ -29,7 +30,9 @@ async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities): entities = [] for sensor_description in plugin.SENSOR_TYPES: - if plugin.matchWithMask(sensor_description.allowedtypes, sensor_description.blacklist): + if plugin.matchWithMask( + sensor_description.allowedtypes, sensor_description.blacklist + ): newdescr = sensor_description sensor = SolaXHttpSensor( coordinator, @@ -44,7 +47,6 @@ async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities): return True - class SolaXHttpSensor(CoordinatorEntity, SensorEntity): """Representation of an SolaX Http sensor.""" @@ -54,13 +56,13 @@ def __init__( platform_name, device_info, description: BaseHttpSensorEntityDescription, - )->None: + ) -> None: """Initialize the sensor.""" super().__init__(coordinator, context=description) self._platform_name = platform_name self._attr_device_info = device_info self.entity_description: BaseHttpSensorEntityDescription = description - self._value=None + self._value = None async def async_added_to_hass(self): """Register callbacks.""" @@ -89,5 +91,3 @@ def unique_id(self) -> Optional[str]: def native_value(self): """Return the state of the sensor.""" return self._value - - diff --git a/custom_components/solax_http/time.py b/custom_components/solax_http/time.py index c2eb253..d897286 100644 --- a/custom_components/solax_http/time.py +++ b/custom_components/solax_http/time.py @@ -4,17 +4,18 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from .const import ATTR_MANUFACTURER, DOMAIN, BaseHttpTimeEntityDescription -from .const import plugin_base +from .plugin_base import plugin_base from .coordinator import SolaxHttpUpdateCoordinator from typing import Any, Dict, Optional import logging _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities) -> None: + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities) -> None: name = entry.options[CONF_NAME] coordinator: SolaxHttpUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - plugin:plugin_base=coordinator.plugin + plugin: plugin_base = coordinator.plugin device_info = { "identifiers": {(DOMAIN, name)}, @@ -24,35 +25,31 @@ async def async_setup_entry(hass:HomeAssistant, entry, async_add_entities) -> No entities = [] for time_info in plugin.TIME_TYPES: - if plugin.matchWithMask(time_info.allowedtypes,time_info.blacklist): - time = SolaXHttpTime( - coordinator, - name, - device_info, - time_info - ) + if plugin.matchWithMask(time_info.allowedtypes, time_info.blacklist): + time = SolaXHttpTime(coordinator, name, device_info, time_info) entities.append(time) async_add_entities(entities) return True + class SolaXHttpTime(CoordinatorEntity, TimeEntity): """Representation of an SolaX Http time.""" def __init__( - self, - coordinator:SolaxHttpUpdateCoordinator, - platform_name, - device_info, - description: BaseHttpTimeEntityDescription, + self, + coordinator: SolaxHttpUpdateCoordinator, + platform_name, + device_info, + description: BaseHttpTimeEntityDescription, ) -> None: """Initialize the time.""" super().__init__(coordinator, context=description) self._platform_name = platform_name self._attr_device_info = device_info self.entity_description = description - self._value:datetime.time=None + self._value: datetime.time = None async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -73,7 +70,6 @@ def name(self): """Return the name.""" return f"{self._platform_name} {self.entity_description.name}" - @property def unique_id(self) -> Optional[str]: return f"{self._platform_name}_{self.entity_description.key}" @@ -82,9 +78,10 @@ def unique_id(self) -> Optional[str]: def native_value(self) -> datetime.time: return self._value - async def async_set_value(self, value: datetime.time) -> None: """Change the time value.""" payload = value - success=await self.coordinator.write_register(self.entity_description.register, payload) + success = await self.coordinator.write_register( + self.entity_description.register, payload + ) await self.coordinator.async_request_refresh() From e2ebf928c0da253b391d070ad420f73f00561b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20T=C5=99e=C5=A1t=C3=ADk?= Date: Thu, 19 Dec 2024 14:23:26 +0100 Subject: [PATCH 2/4] Mapping notes for HVC --- doc/Mapping.xlsx | Bin 18703 -> 17137 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/Mapping.xlsx b/doc/Mapping.xlsx index baca0a125ec1d93dd53c77f99d60389af7d4b999..060e725d63fe88c48df7f09be2161385b19cd3eb 100644 GIT binary patch delta 10651 zcmZ{K1yof{8!jOw-6-HekdW>!>F(|n>6T_ocOJSsr9m151XQ{^4&B{((Xan|FY7GU zUK4LV?=v%dc2q3%aR7AX52taZmi0VXZ!Nn8C)sIz!2Ci`RG?)Wpcvdtw#XJdi!mVE5fc&XFA z?!)%@aQ6chRu0ujG8oa&up*A<|zfr9;=Yl6&C@I9U1f=NUMr z`He;MY7gTV9NG;@Jd7@e>{6MD6r&FpIs z?zNB98Eh#kDvcxSRw@|O+~BS1$oEmpUZH_07jTwQprRed|Mr~hE$IoBZE#7r+&DH% z1l|(4f4jnRAzpn8Hk@w%R%lC=_xQ;`1OqqCsbp4CSXf>L-N~-jS(d#E%^@c; z{*ZMsZJ~GcRKBb7QW)WL>N0Vz5BdzZ~0L9IXed7Vgp=L3)LUx!J z)t9n;8dG^(MzLv-PYh0I>oAwwvoJJh%M9#OI+t`P9&?*(&fWz4aA8ZLrs8;$WJ0s) zh^{x=*E0yuZa0ucZf8E+`ZnfEy{iuzoYB6`OAQP@d%IFC!rdeM7*6G|jO=B180GdSpOtnt4){gJ`1Q{E*^I}l+@nan$3Qtrb6pGP>-(X*bkfQ6MQEs7ZO4@JQrs

o%Ef1G)G7X?AQ1z|U} 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 delta 12280 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 Date: Thu, 19 Dec 2024 14:38:08 +0100 Subject: [PATCH 3/4] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f50258..e02c480 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ Please write only issues specific to Http API here. All other stuff is common and should be discussed in https://github.com/wills106/homeassistant-solax-modbus # Supported devices -Only SolaX EV Charger X3 is supported. +G1 SolaX EV Charger X3 is supported. +G2 SolaX Smart EV Charger support is under development and testing. Let me know if you have other device and you are interested in integrating it. Physical device is required as there is no documentation available. # Installation From 9f083471dbc3b022b8f9c5b76101e25fbd29d2fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20T=C5=99e=C5=A1t=C3=ADk?= Date: Thu, 19 Dec 2024 15:24:12 +0100 Subject: [PATCH 4/4] Mapping first Options for G2 (read only) --- .../solax_http/entity_definitions.py | 2 +- .../solax_http/plugin_solax_ev_charger_g2.py | 86 +++++++++--------- doc/Mapping.xlsx | Bin 17137 -> 18621 bytes 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/custom_components/solax_http/entity_definitions.py b/custom_components/solax_http/entity_definitions.py index 658c2ff..556801c 100644 --- a/custom_components/solax_http/entity_definitions.py +++ b/custom_components/solax_http/entity_definitions.py @@ -476,7 +476,7 @@ class SolaXEVChargerHttpSensorEntityDescription(BaseHttpSensorEntityDescription) native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, - allowedtypes=G1, + allowedtypes=G1 | G2, ), # SolaXEVChargerHttpSensorEntityDescription( # name = "Control Command", diff --git a/custom_components/solax_http/plugin_solax_ev_charger_g2.py b/custom_components/solax_http/plugin_solax_ev_charger_g2.py index c7c1133..bb148ce 100644 --- a/custom_components/solax_http/plugin_solax_ev_charger_g2.py +++ b/custom_components/solax_http/plugin_solax_ev_charger_g2.py @@ -90,20 +90,20 @@ def map_data(self, descr, data) -> any: # return_value = datetime.datetime( # 2000 + year, month, day, hour, minute, second # ).astimezone() - case 0x600: - return_value = Info.get(2) - case 0x60C: - return_value = Set.get(0) - case 0x60D: - return_value = Set.get(1) - case 0x60E: - return_value = Set.get(2) - case 0x60F: - return_value = Set.get(3) - case 0x610: - return_value = Set.get(4) - case 0x613: - return_value = Set.get(11) + # case 0x600: #SN + # return_value = Info.get(2) + # case 0x60C: #Grid Data Source + # return_value = Set.get(0) + # case 0x60D: #Mode + # return_value = Set.get(1) + # case 0x60E: #Eco Gear + # return_value = Set.get(2) + # case 0x60F: #Green Gear + # return_value = Set.get(3) + # case 0x610: #StartChargeMode + # return_value = Set.get(4) + # case 0x613: #Smart boost Type + # return_value = Set.get(11) # case 0x615: # return_value=Data[] # case 0x616: @@ -112,31 +112,31 @@ def map_data(self, descr, data) -> any: # return_value=[Data[38]H, Data[38]L, Data[37]H, Data[37]L, Data[36]H, Data[36]L] # case 0x625: # return_value = Data.get(65) - case 0x628: - return_value = Set.get(76) - case 0x634: - val = Set.get(12) - if val is not None: - hour = val >> 8 - minute = val & 0x00FF - if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: - return_value = datetime.time(hour, minute) - case 0x636: - val = Set.get(13) - if val is not None: - hour = val >> 8 - minute = val & 0x00FF - if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: - return_value = datetime.time(hour, minute) - case 0x638: - val = Set.get(15) - if val is not None: - hour = val >> 8 - minute = val & 0x00FF - if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: - return_value = datetime.time(hour, minute) - case 0x63A: - return_value = Set.get(14) + case 0x628: # Max Charge Current + return_value = Set.get(3) + # case 0x634: #Boost time start + # val = Set.get(12) + # if val is not None: + # hour = val >> 8 + # minute = val & 0x00FF + # if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: + # return_value = datetime.time(hour, minute) + # case 0x636: + # val = Set.get(13) + # if val is not None: + # hour = val >> 8 + # minute = val & 0x00FF + # if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: + # return_value = datetime.time(hour, minute) + # case 0x638: #Boost time + # val = Set.get(15) + # if val is not None: + # hour = val >> 8 + # minute = val & 0x00FF + # if hour >= 0 and hour < 24 and minute >= 0 and minute < 60: + # return_value = datetime.time(hour, minute) + # case 0x63A: #Boost charge + # return_value = Set.get(14) case 0x0: return_value = Data.get(3) case 0x1: @@ -188,10 +188,10 @@ def map_data(self, descr, data) -> any: # return_value = Data.get(24) case 0x1D: return_value = Data.get(0) - case 0x25: - ver = str(Set.get(19)) - if ver is not None: - return_value = f"{ver[0]}.{ver[1:]}" + # case 0x25: #Firmware/ Factory reset + # ver = str(Set.get(19)) + # if ver is not None: + # return_value = f"{ver[0]}.{ver[1:]}" # case 0x2B: #Charge time # datH = Data.get(81) # datL = Data.get(80) diff --git a/doc/Mapping.xlsx b/doc/Mapping.xlsx index 060e725d63fe88c48df7f09be2161385b19cd3eb..720192bad70cdfd084059fe9651399dd5fb5fcde 100644 GIT binary patch delta 10144 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 OUxNEcNKr5E-u@5D)7lUK delta 9296 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 U2Ij3=eB{>wxs3>sxc&G40De3sO#lD@