diff --git a/README.md b/README.md index 3426bca..511ae41 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ As you might have noticed I'm having trouble to spend enough time on maintaining # Control Beta Test -This repo includes a beta version of device control using the same API as the SolisCloud app. This opeartes slighty differently depending on your HMI firmware version. This should be detected automatically: +This repo includes a beta version of device control using the same API as the SolisCloud app. This opeartes slighty differently depending on your HMI firmware version. This should be detected automatically. + +Please report any issues via https://github.com/fboundy/solis-sensor/issues ## Version 4A00 and Earlier diff --git a/custom_components/solis/__init__.py b/custom_components/solis/__init__.py index 244106c..5f3b6c7 100644 --- a/custom_components/solis/__init__.py +++ b/custom_components/solis/__init__.py @@ -40,6 +40,13 @@ Platform.BUTTON, ] +CONTROL_PLATFORMS = [ + Platform.SELECT, + Platform.NUMBER, + Platform.TIME, + Platform.BUTTON, +] + async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the Solis component from configuration.yaml.""" @@ -93,7 +100,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = service # Forward the setup to the sensor platform. - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR]) + # while not service.discovery_complete: + # asyncio.sleep(1) + _LOGGER.debug("Sensor setup complete") + await hass.config_entries.async_forward_entry_setups(entry, CONTROL_PLATFORMS) return True diff --git a/custom_components/solis/button.py b/custom_components/solis/button.py index 24ee148..a4888de 100644 --- a/custom_components/solis/button.py +++ b/custom_components/solis/button.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _LOGGER.debug(f"Domain: {DOMAIN}") service = hass.data[DOMAIN][config_entry.entry_id] - _LOGGER.info(f"Waiting for discovery of Button entities for plant {plant_id}") + _LOGGER.info(f"Waiting for discovery of controls for plant {plant_id}") await asyncio.sleep(8) attempts = 0 while (attempts < RETRIES) and (not service.has_controls): @@ -37,32 +37,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn if service.has_controls: entities = [] _LOGGER.debug(f"Plant ID {plant_id} has controls:") - _LOGGER.debug(service.controls) for inverter_sn in service.controls: - _LOGGER.debug(f"Waiting for inverter {inverter_sn} HMI status") - attempts = 0 - while service.api._hmi_fb00[inverter_sn] is None: - _LOGGER.debug(f" Attempt {attempts} failed") - await asyncio.sleep(RETRY_WAIT) - attempts += 1 - hmi_fb00 = service.api._hmi_fb00[inverter_sn] - _LOGGER.debug(f"Inverter SN {inverter_sn} HMI status {hmi_fb00}") - for cid in service.controls[inverter_sn]: - _LOGGER.debug(f">>> {cid:4s}") - for index, entity in enumerate(ALL_CONTROLS[hmi_fb00][cid]): - _LOGGER.debug(f">>> {index} {entity.name} {isinstance(entity, SolisButtonEntityDescription)}") - if isinstance(entity, SolisButtonEntityDescription): - _LOGGER.debug(f"Adding Button entity {entity.name} for inverter Sn {inverter_sn} cid {cid}") - entities.append( - SolisButtonEntity( - service, - config_entry.data["name"], - inverter_sn, - cid, - entity, - index, - ) - ) + for cid, index, entity, button, intial_value in service.controls[inverter_sn]["button"]: + entities.append( + SolisButtonEntity( + service, + config_entry.data["name"], + inverter_sn, + cid, + entity, + index, + ) + ) if len(entities) > 0: _LOGGER.debug(f"Creating {len(entities)} Button entities") @@ -94,6 +80,8 @@ async def async_press(self) -> None: """Handle the button press.""" for entity in self._entities: _LOGGER.debug(f"{entity.name:s} {entity.to_string:s} {entity.index}") - value = self. _joiner.join([entity.to_string for entity in self._entities]) + # Sort the entities by their index + items = sorted({entity.index: entity.to_string for entity in self._entities}.items()) + value = self._joiner.join([x[1] for x in items]) _LOGGER.debug(f"{self._cid} {value}") - await self.write_control_data(value) \ No newline at end of file + await self.write_control_data(value) diff --git a/custom_components/solis/control_const.py b/custom_components/solis/control_const.py index c1b5468..d66dd0d 100644 --- a/custom_components/solis/control_const.py +++ b/custom_components/solis/control_const.py @@ -3,14 +3,15 @@ from homeassistant.components.number import NumberDeviceClass from homeassistant.components.time import TimeEntityDescription from homeassistant.components.button import ButtonEntityDescription -from homeassistant.const import PERCENTAGE +from homeassistant.const import PERCENTAGE, UnitOfElectricCurrent +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import DeviceInfo from dataclasses import dataclass from datetime import datetime +import logging from .const import ( DOMAIN, - LAST_UPDATED, SERIAL, API_NAME, EMPTY_ATTR, @@ -20,6 +21,7 @@ RETRY_WAIT = 10 HMI_CID = "6798" +_LOGGER = logging.getLogger(__name__) class SolisBaseControlEntity: @@ -72,9 +74,15 @@ def split(self, value): if len(self._splitter) > 0: # if there's more than one split string then replace all of the later ones with the first before we split for x in self._splitter[1:]: - value.replace(x, self._splitter[0]) - value = value.split(self._splitter[0])[self._index] - return value + value = value.replace(x, self._splitter[0]) + values = value.split(self._splitter[0]) + + if self._index <= len(values): + return values[self._index] + else: + _LOGGER.warning(f"Unable to retrieve item {self._index:d} from {value} for {self._key}") + else: + return value @dataclass @@ -100,8 +108,160 @@ class SolisButtonEntityDescription(ButtonEntityDescription): # Control types dict[bool: dict] where key is HMI_4B00 flag +CONTROL_TYPES = { + "time": SolisTimeEntityDescription, + "number": SolisNumberEntityDescription, + "select": SolisSelectEntityDescription, + "button": SolisButtonEntityDescription, +} + ALL_CONTROLS = { True: { + # 103 is still available with 4B00 but it doesn't do anything included here for testing only + # "103": [ + # SolisNumberEntityDescription( + # name="Timed Charge Current 1", + # key="timed_charge_current_1", + # native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + # device_class=SensorDeviceClass.CURRENT, + # icon="mdi:current-dc", + # native_min_value=0, + # native_max_value=100, + # native_step=1, + # splitter=(",", "-"), + # ), + # SolisNumberEntityDescription( + # name="Timed Discharge Current 1", + # key="timed_discharge_current_1", + # native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + # device_class=SensorDeviceClass.CURRENT, + # icon="mdi:current-dc", + # native_min_value=0, + # native_max_value=100, + # native_step=1, + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Charge Start 1", + # key="timed_charge_start_1", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Charge End 1", + # key="timed_charge_end_1", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Discharge Start 1", + # key="timed_discharge_start_1", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Discharge End 1", + # key="timed_discharge_end_1", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisNumberEntityDescription( + # name="Timed Charge Current 2", + # key="timed_charge_current_2", + # native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + # device_class=SensorDeviceClass.CURRENT, + # icon="mdi:current-dc", + # native_min_value=0, + # native_max_value=100, + # native_step=1, + # splitter=(",", "-"), + # ), + # SolisNumberEntityDescription( + # name="Timed Discharge Current 2", + # key="timed_discharge_current_2", + # native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + # device_class=SensorDeviceClass.CURRENT, + # icon="mdi:current-dc", + # native_min_value=0, + # native_max_value=100, + # native_step=1, + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Charge Start 2", + # key="timed_charge_start_2", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Charge End 2", + # key="timed_charge_end_2", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Discharge Start 2", + # key="timed_discharge_start_2", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Discharge End 2", + # key="timed_discharge_end_2", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisNumberEntityDescription( + # name="Timed Charge Current 3", + # key="timed_charge_current_3", + # native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + # device_class=SensorDeviceClass.CURRENT, + # icon="mdi:current-dc", + # native_min_value=0, + # native_max_value=100, + # native_step=1, + # splitter=(",", "-"), + # ), + # SolisNumberEntityDescription( + # name="Timed Discharge Current 3", + # key="timed_discharge_current_3", + # native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + # device_class=SensorDeviceClass.CURRENT, + # icon="mdi:current-dc", + # native_min_value=0, + # native_max_value=100, + # native_step=1, + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Charge Start 3", + # key="timed_charge_start_3", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Charge End 3", + # key="timed_charge_end_3", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Discharge Start 3", + # key="timed_discharge_start_3", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisTimeEntityDescription( + # name="Timed Discharge End 3", + # key="timed_discharge_end_3", + # icon="mdi:clock", + # splitter=(",", "-"), + # ), + # SolisButtonEntityDescription( + # name="Update Timed Charge/Discharge", + # key="update_timed_charge_discharge", + # ), + # ], "157": [ SolisNumberEntityDescription( name="Backup SOC", @@ -156,8 +316,8 @@ class SolisButtonEntityDescription(ButtonEntityDescription): ], "5928": [ SolisNumberEntityDescription( - name="Timed Charge SOC", - key="timed_charge_soc", + name="Timed Charge SOC 1", + key="timed_charge_soc_1", native_unit_of_measurement=PERCENTAGE, device_class=NumberDeviceClass.BATTERY, icon="mdi:battery-sync", @@ -166,26 +326,649 @@ class SolisButtonEntityDescription(ButtonEntityDescription): native_step=1, ) ], + "5948": [ + SolisNumberEntityDescription( + name="Timed Charge Current 1", + key="timed_charge_current_1", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], "5946": [ SolisTimeEntityDescription( - name="Timed Charge Start", - key="timed_charge_start", + name="Timed Charge Start 1", + key="timed_charge_start_1", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Charge End 1", + key="timed_charge_end_1", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Charge 1", + key="update_timed_charge_1", + ), + ], + "5965": [ + SolisNumberEntityDescription( + name="Timed Discharge SOC 1", + key="timed_discharge_soc_1", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5967": [ + SolisNumberEntityDescription( + name="Timed Discharge Current 1", + key="timed_discharge_current_1", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5964": [ + SolisTimeEntityDescription( + name="Timed Discharge Start 1", + key="timed_discharge_start_1", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge End 1", + key="timed_discharge_end_1", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Discharge 1", + key="update_timed_discharge_1", + ), + ], + # ======================= Slot 2 ================================= + "5929": [ + SolisNumberEntityDescription( + name="Timed Charge SOC 2", + key="timed_charge_soc_2", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5951": [ + SolisNumberEntityDescription( + name="Timed Charge Current 2", + key="timed_charge_current_2", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5949": [ + SolisTimeEntityDescription( + name="Timed Charge Start 2", + key="timed_charge_start_2", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Charge End 2", + key="timed_charge_end_2", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Charge 2", + key="update_timed_charge_2", + ), + ], + "5969": [ + SolisNumberEntityDescription( + name="Timed Discharge SOC 2", + key="timed_discharge_soc_2", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5971": [ + SolisNumberEntityDescription( + name="Timed Discharge Current", + key="timed_discharge_current_2", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5968": [ + SolisTimeEntityDescription( + name="Timed Discharge Start 2", + key="timed_discharge_start_2", icon="mdi:clock", splitter=("-"), ), SolisTimeEntityDescription( - name="Timed Charge End", - key="timed_charge_end", + name="Timed Discharge End 2", + key="timed_discharge_end_2", icon="mdi:clock", splitter=("-"), ), SolisButtonEntityDescription( - name="Timed Charge", - key="timed_charge", + name="Update Timed Discharge 2", + key="update_timed_discharge_2", + ), + ], + # ======================= Slot 3 ================================= + "5930": [ + SolisNumberEntityDescription( + name="Timed Charge SOC 3", + key="timed_charge_soc_3", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5954": [ + SolisNumberEntityDescription( + name="Timed Charge Current 3", + key="timed_charge_current_3", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5952": [ + SolisTimeEntityDescription( + name="Timed Charge Start 3", + key="timed_charge_start_3", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Charge End 3", + key="timed_charge_end_3", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Charge 3", + key="update_timed_charge_3", + ), + ], + "5973": [ + SolisNumberEntityDescription( + name="Timed Discharge SOC 3", + key="timed_discharge_soc_3", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5975": [ + SolisNumberEntityDescription( + name="Timed Discharge Current 3", + key="timed_discharge_current_3", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5972": [ + SolisTimeEntityDescription( + name="Timed Discharge Start 3", + key="timed_discharge_start_3", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge End 3", + key="timed_discharge_end_3", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Discharge 3", + key="update_timed_discharge_3", + ), + ], + # ======================= Slot 4 ================================= + "5931": [ + SolisNumberEntityDescription( + name="Timed Charge SOC 4", + key="timed_charge_soc_4", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5957": [ + SolisNumberEntityDescription( + name="Timed Charge Current 4", + key="timed_charge_current_4", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5955": [ + SolisTimeEntityDescription( + name="Timed Charge Start 4", + key="timed_charge_start_4", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Charge End 4", + key="timed_charge_end_4", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Charge 4", + key="update_timed_charge_4", + ), + ], + "5977": [ + SolisNumberEntityDescription( + name="Timed Discharge SOC 4", + key="timed_discharge_soc_4", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5979": [ + SolisNumberEntityDescription( + name="Timed Discharge Current 4", + key="timed_discharge_current_4", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5976": [ + SolisTimeEntityDescription( + name="Timed Discharge Start 4", + key="timed_discharge_start_4", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge End 4", + key="timed_discharge_end_4", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Discharge 4", + key="update_timed_discharge_4", + ), + ], + # ======================= Slot 5 ================================= + "5932": [ + SolisNumberEntityDescription( + name="Timed Charge SOC 5", + key="timed_charge_soc_5", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5960": [ + SolisNumberEntityDescription( + name="Timed Charge Current 5", + key="timed_charge_current_5", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5958": [ + SolisTimeEntityDescription( + name="Timed Charge Start 5", + key="timed_charge_start_5", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Charge End 5", + key="timed_charge_end_5", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Charge 5", + key="update_timed_charge_5", + ), + ], + "5981": [ + SolisNumberEntityDescription( + name="Timed Discharge SOC 5", + key="timed_discharge_soc_5", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5983": [ + SolisNumberEntityDescription( + name="Timed Discharge Current 5", + key="timed_discharge_current_5", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5980": [ + SolisTimeEntityDescription( + name="Timed Discharge Start 5", + key="timed_discharge_start_5", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge End 5", + key="timed_discharge_end_5", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Discharge 5", + key="update_timed_discharge_5", + ), + ], + # ======================= Slot 6 ================================= + "5933": [ + SolisNumberEntityDescription( + name="Timed Charge SOC 6", + key="timed_charge_soc_6", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5963": [ + SolisNumberEntityDescription( + name="Timed Charge Current 6", + key="timed_charge_current_6", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5961": [ + SolisTimeEntityDescription( + name="Timed Charge Start 6", + key="timed_charge_start_6", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Charge End 6", + key="timed_charge_end_6", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Charge 6", + key="update_timed_charge_6", + ), + ], + "5984": [ + SolisNumberEntityDescription( + name="Timed Discharge SOC 6", + key="timed_discharge_soc_6", + native_unit_of_measurement=PERCENTAGE, + device_class=NumberDeviceClass.BATTERY, + icon="mdi:battery-sync", + native_min_value=10, + native_max_value=100, + native_step=1, + ) + ], + "5986": [ + SolisNumberEntityDescription( + name="Timed Discharge Current 6", + key="timed_discharge_current_6", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:battery-sync", + native_min_value=0, + native_max_value=100, + native_step=1, + ) + ], + "5987": [ + SolisTimeEntityDescription( + name="Timed Discharge Start 6", + key="timed_discharge_start_6", + icon="mdi:clock", + splitter=("-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge End 6", + key="timed_discharge_end_6", + icon="mdi:clock", + splitter=("-"), + ), + SolisButtonEntityDescription( + name="Update Timed Discharge 6", + key="update_timed_discharge_6", ), ], }, False: { + "103": [ + SolisNumberEntityDescription( + name="Timed Charge Current 1", + key="timed_charge_current_1", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-dc", + native_min_value=0, + native_max_value=100, + native_step=1, + splitter=(",", "-"), + ), + SolisNumberEntityDescription( + name="Timed Discharge Current 1", + key="timed_discharge_current_1", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-dc", + native_min_value=0, + native_max_value=100, + native_step=1, + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Charge Start 1", + key="timed_charge_start_1", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Charge End 1", + key="timed_charge_end_1", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge Start 1", + key="timed_discharge_start_1", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge End 1", + key="timed_discharge_end_1", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisNumberEntityDescription( + name="Timed Charge Current 2", + key="timed_charge_current_2", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-dc", + native_min_value=0, + native_max_value=100, + native_step=1, + splitter=(",", "-"), + ), + SolisNumberEntityDescription( + name="Timed Discharge Current 2", + key="timed_discharge_current_2", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-dc", + native_min_value=0, + native_max_value=100, + native_step=1, + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Charge Start 2", + key="timed_charge_start_2", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Charge End 2", + key="timed_charge_end_2", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge Start 2", + key="timed_discharge_start_2", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge End 2", + key="timed_discharge_end_2", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisNumberEntityDescription( + name="Timed Charge Current 3", + key="timed_charge_current_3", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-dc", + native_min_value=0, + native_max_value=100, + native_step=1, + splitter=(",", "-"), + ), + SolisNumberEntityDescription( + name="Timed Discharge Current 3", + key="timed_discharge_current_3", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + icon="mdi:current-dc", + native_min_value=0, + native_max_value=100, + native_step=1, + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Charge Start 3", + key="timed_charge_start_3", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Charge End 3", + key="timed_charge_end_3", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge Start 3", + key="timed_discharge_start_3", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisTimeEntityDescription( + name="Timed Discharge End 3", + key="timed_discharge_end_3", + icon="mdi:clock", + splitter=(",", "-"), + ), + SolisButtonEntityDescription( + name="Update Timed Charge/Discharge", + key="update_timed_charge_discharge", + ), + ], "157": [ SolisNumberEntityDescription( name="Backup SOC", @@ -227,13 +1010,19 @@ class SolisButtonEntityDescription(ButtonEntityDescription): name="Energy Storage Control Switch", key="energy_storage_control_switch", option_dict={ - "1": "Self-Use - No Grid Charging", - "5": "Off-Grid Mode", - "9": "Battery Awaken - No Grid Charging", - "33": "Self-Use", - "41": "Battery Awaken", - "49": "Backup/Reserve", - "64": "Feed-in priority", + 1: "Self-Use - No Grid Charging", + 3: "Timed Charge/Discharge - No Grid Charging", + 17: "Backup/Reserve - No Grid Charging", + 33: "Self-Use - No Timed Charge/Discharge", + 35: "Self-Use", + 37: "Off-Grid Mode", + 41: "Battery Awaken", + 43: "Battery Awaken + Timed Charge/Discharge", + 49: "Backup/Reserve - No Timed Charge/Discharge", + 51: "Backup/Reserve", + 64: "Feed-in priority - No Grid Charging", + 96: "Feed-in priority - No Timed Charge/Discharge", + 98: "Feed-in priority", }, icon="mdi:dip-switch", ) diff --git a/custom_components/solis/number.py b/custom_components/solis/number.py index b0f62b6..0d9b0d5 100644 --- a/custom_components/solis/number.py +++ b/custom_components/solis/number.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _LOGGER.debug(f"Domain: {DOMAIN}") service = hass.data[DOMAIN][config_entry.entry_id] - _LOGGER.info(f"Waiting for discovery of Number entities for plant {plant_id}") + _LOGGER.info(f"Waiting for discovery of controls for plant {plant_id}") await asyncio.sleep(8) attempts = 0 while (attempts < RETRIES) and (not service.has_controls): @@ -38,30 +38,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn if service.has_controls: entities = [] _LOGGER.debug(f"Plant ID {plant_id} has controls:") - _LOGGER.debug(service.controls) for inverter_sn in service.controls: - _LOGGER.debug(f"Waiting for inverter {inverter_sn} HMI status") - attempts = 0 - while service.api._hmi_fb00[inverter_sn] is None: - _LOGGER.debug(f" Attempt {attempts} failed") - await asyncio.sleep(RETRY_WAIT) - attempts += 1 - hmi_fb00 = service.api._hmi_fb00[inverter_sn] - _LOGGER.debug(f"Inverter SN {inverter_sn} HMI status {hmi_fb00}") - for cid in service.controls[inverter_sn]: - for index, entity in enumerate(ALL_CONTROLS[hmi_fb00][cid]): - if isinstance(entity, SolisNumberEntityDescription): - _LOGGER.debug(f"Adding number entity {entity.name} for inverter Sn {inverter_sn} cid {cid}") - entities.append( - SolisNumberEntity( - service, - config_entry.data["name"], - inverter_sn, - cid, - entity, - index, - ) - ) + for cid, index, entity, button, initial_value in service.controls[inverter_sn]["number"]: + entities.append( + SolisNumberEntity( + service, + config_entry.data["name"], + inverter_sn, + cid, + entity, + index, + button, + initial_value, + ) + ) if len(entities) > 0: _LOGGER.debug(f"Creating {len(entities)} number entities") @@ -76,7 +66,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn class SolisNumberEntity(SolisBaseControlEntity, ServiceSubscriber, NumberEntity): - def __init__(self, service: InverterService, config_name, inverter_sn, cid, number_info, index): + def __init__( + self, + service: InverterService, + config_name, + inverter_sn: str, + cid: str, + number_info, + index: int, + button: bool, + initial_value, + ): super().__init__(service, config_name, inverter_sn, cid, number_info) self._attr_native_value = 0 self._attr_native_max_value = number_info.native_max_value @@ -86,6 +86,9 @@ def __init__(self, service: InverterService, config_name, inverter_sn, cid, numb self._icon = number_info.icon self._splitter = number_info.splitter self._index = index + self._button = button + if initial_value is not None: + self.do_update(initial_value, datetime.now()) # Subscribe to the service with the cid as the index service.subscribe(self, inverter_sn, str(cid)) @@ -103,12 +106,21 @@ def do_update(self, value, last_updated): return True return False + @property def to_string(self): - return str(self._attr_native_value) + if self._attr_native_step >= 1: + return f"{int(self._attr_native_value):d}" + elif self._attr_native_step >= 0.1: + return f"{self._attr_native_value:0.1f}" + elif self._attr_native_step >= 0.01: + return f"{self._attr_native_value:0.2f}" + else: + return f"{self._attr_native_value:f}" async def async_set_native_value(self, value: float) -> None: _LOGGER.debug(f"async_set_native_value for {self._name}") self._attr_native_value = value self._attributes[LAST_UPDATED] = datetime.now() self.async_write_ha_state() - await self.write_control_data(str(value)) + if not self._button: + await self.write_control_data(str(value)) diff --git a/custom_components/solis/select.py b/custom_components/solis/select.py index e6e01ae..8ad620c 100644 --- a/custom_components/solis/select.py +++ b/custom_components/solis/select.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn # _LOGGER.debug(f"config_entry.data: {config_entry.data}") service = hass.data[DOMAIN][config_entry.entry_id] - _LOGGER.info(f"Waiting for discovery of Select entities for plant {plant_id}") + _LOGGER.info(f"Waiting for discovery of controls for plant {plant_id}") await asyncio.sleep(8) attempts = 0 while (attempts < RETRIES) and (not service.has_controls): @@ -37,29 +37,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn entities = [] _LOGGER.debug(f"Plant ID {plant_id} has controls:") for inverter_sn in service.controls: - _LOGGER.debug(f"Waiting for inverter {inverter_sn} HMI status") - attempts = 0 - while service.api._hmi_fb00[inverter_sn] is None: - _LOGGER.debug(f" Attempt {attempts} failed") - await asyncio.sleep(RETRY_WAIT) - attempts += 1 - hmi_fb00 = service.api._hmi_fb00[inverter_sn] - _LOGGER.debug(f"Inverter SN {inverter_sn} HMI status {hmi_fb00}") - - for cid in service.controls[inverter_sn]: - for index, entity in enumerate(ALL_CONTROLS[hmi_fb00][cid]): - if isinstance(entity, SolisSelectEntityDescription): - _LOGGER.debug(f"Adding select entity {entity.name} for inverter Sn {inverter_sn} cid {cid}") - entities.append( - SolisSelectEntity( - service, - config_entry.data["name"], - inverter_sn, - cid, - entity, - index, - ) - ) + for cid, index, entity, button, initial_value in service.controls[inverter_sn]["select"]: + entities.append( + SolisSelectEntity( + service, + config_entry.data["name"], + inverter_sn, + cid, + entity, + index, + initial_value, + ) + ) if len(entities) > 0: _LOGGER.debug(f"Creating {len(entities)} sensor entities") @@ -74,7 +63,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn class SolisSelectEntity(SolisBaseControlEntity, ServiceSubscriber, SelectEntity): - def __init__(self, service: InverterService, config_name, inverter_sn, cid, select_info, index): + def __init__( + self, + service: InverterService, + config_name, + inverter_sn, + cid, + select_info, + index, + initial_value, + ): super().__init__(service, config_name, inverter_sn, cid, select_info) self._option_dict = select_info.option_dict self._reverse_dict = {self._option_dict[k]: str(k) for k in self._option_dict} @@ -82,6 +80,8 @@ def __init__(self, service: InverterService, config_name, inverter_sn, cid, sele self._attr_options = list(select_info.option_dict.values()) self._attr_current_option = None self._index = index + if initial_value is not None: + self.do_update(initial_value, datetime.now()) # Subscribe to the service with the cid as the index service.subscribe(self, inverter_sn, cid) diff --git a/custom_components/solis/service.py b/custom_components/solis/service.py index f7bf875..0520723 100644 --- a/custom_components/solis/service.py +++ b/custom_components/solis/service.py @@ -28,6 +28,7 @@ INVERTER_TIMESTAMP_UPDATE, ) +from .control_const import HMI_CID, ALL_CONTROLS, CONTROL_TYPES # REFRESH CONSTANTS # Match up with the default SolisCloud API resolution of 5 minutes @@ -92,9 +93,10 @@ def __init__(self, portal_config: PortalConfig, hass: HomeAssistant) -> None: self._hass: HomeAssistant = hass self._discovery_callback = None self._discovery_cookie: dict[str, Any] = {} + self._discovery_complete: bool = False self._retry_delay_seconds = 0 self._controllable: bool = False - self._controls: dict[str, Any] = {} + self._controls: dict[str, dict[str, list[tuple]]] = {} if isinstance(portal_config, GinlongConfig): self._api: BaseAPI = GinlongAPI(portal_config) elif isinstance(portal_config, SoliscloudConfig): @@ -127,6 +129,10 @@ def controllable(self) -> bool: def controls(self) -> dict: return self._controls + @property + def discovery_complete(self) -> bool: + return self._discovery_complete + async def _login(self) -> bool: if not self._api.is_online: if await self._api.login(async_get_clientsession(self._hass)): @@ -145,24 +151,44 @@ async def async_discover(self, *_) -> None: capabilities = await self._do_discover() if capabilities: - controls = {} if self.controllable: - for inverter_sn in capabilities: - controls[inverter_sn] = await self._api.get_control_data(inverter_sn) - # controls[inverter_sn] = [cid for cid in controls[inverter_sn] if cid in controls_by_hmi] - - self._controls = controls - _LOGGER.debug(f"controls: {controls}") + inverter_serials = list(capabilities.keys()) + await self._discover_controls(inverter_serials) if self._discovery_callback and self._discovery_cookie: self._discovery_callback(capabilities, self._discovery_cookie) self._retry_delay_seconds = 0 + self._dicovery_complete = True else: self._retry_delay_seconds = min(MAX_RETRY_DELAY_SECONDS, self._retry_delay_seconds + RETRY_DELAY_SECONDS) _LOGGER.warning("Failed to discover, scheduling retry in %s seconds.", self._retry_delay_seconds) await self._logout() self.schedule_discovery(self._discovery_callback, self._discovery_cookie, self._retry_delay_seconds) + async def _discover_controls(self, inverter_serials: list[str]): + _LOGGER.debug(f"Starting controls discovery") + controls = {} + control_lookup = {CONTROL_TYPES[platform]: platform for platform in CONTROL_TYPES} + for inverter_sn in inverter_serials: + controls[inverter_sn] = {platform: [] for platform in CONTROL_TYPES} + await self._api.get_control_data(inverter_sn, HMI_CID) + hmi_flag = self._api.hmi_fb00(inverter_sn) + _LOGGER.debug(f"Inverter SN {inverter_sn} HMI status {hmi_flag}") + control_desciptions = ALL_CONTROLS[hmi_flag] + for cid in control_desciptions: + button = len(control_desciptions[cid]) > 1 + initial_value = await self._api.get_control_data(inverter_sn, cid) + initial_value = initial_value.get(cid, None) + for index, entity_description in enumerate(control_desciptions[cid]): + entity_type = control_lookup[type(entity_description)] + controls[inverter_sn][entity_type].append((cid, index, entity_description, button, initial_value)) + _LOGGER.debug( + f"Adding {entity_type:s} entity {entity_description.name:s} for inverter Sn {inverter_sn:s} cid {cid:s} with index {index:d}" + ) + + self._controls = controls + _LOGGER.debug(f"Controls discovery complete") + async def _do_discover(self) -> dict[str, list[str]]: """Discover for all inverters the attributes it supports""" capabilities: dict[str, list[str]] = {} @@ -172,7 +198,7 @@ async def _do_discover(self) -> dict[str, list[str]]: if inverters is None: return capabilities for inverter_serial in inverters: - data = await self._api.fetch_inverter_data(inverter_serial) + data = await self._api.fetch_inverter_data(inverter_serial, controls=False) if data is not None: capabilities[inverter_serial] = data.keys() return capabilities diff --git a/custom_components/solis/soliscloud_api.py b/custom_components/solis/soliscloud_api.py index 5426472..49b756d 100644 --- a/custom_components/solis/soliscloud_api.py +++ b/custom_components/solis/soliscloud_api.py @@ -46,6 +46,7 @@ MESSAGE = "Message" CONTROL_DELAY = 0.1 +CONTROL_RETRIES = 5 # VALUE_RECORD = '_from_record' # VALUE_ELEMENT = '' @@ -262,6 +263,9 @@ def config(self) -> SoliscloudConfig: """Config this for this API instance.""" return self._config + def hmi_fb00(self, inverter_sn): + return self._hmi_fb00.get(inverter_sn, None) + @property def is_online(self) -> bool: """Returns if we are logged in.""" @@ -341,7 +345,7 @@ async def fetch_inverter_list(self, plant_id: str) -> dict[str, str]: ) return device_ids - async def fetch_inverter_data(self, inverter_serial: str) -> GinlongData | None: + async def fetch_inverter_data(self, inverter_serial: str, controls=True) -> GinlongData | None: """ Fetch data for given inverter. Collect available data from payload and store as GinlongData object @@ -362,7 +366,7 @@ async def fetch_inverter_data(self, inverter_serial: str) -> GinlongData | None: self._collect_inverter_data(payload) # if payload2 is not None: # self._collect_station_list_data(payload2) - if self._token != "": + if (self._token != "") and controls: _LOGGER.debug(f"Fetching control data for SN:{inverter_serial}") control_data = await self.get_control_data(inverter_serial) @@ -457,21 +461,28 @@ async def get_control_data(self, device_serial: str, cid="") -> dict[str, Any] | if cid == "": controls = ALL_CONTROLS[self._hmi_fb00[device_serial]] else: - controls=[cid] + controls = [cid] for cid in controls: params = {"inverterSn": str(device_serial), "cid": str(cid)} - result = await self._post_data_json(AT_READ, params, csrf=True) - if result[SUCCESS] is True: - jsondata = result[CONTENT] - if jsondata["code"] == "0": - _LOGGER.debug(f" cid: {str(cid):5s} - {jsondata.get('data',{}).get('msg','')}") - control_data[str(cid)] = jsondata.get("data", {}).get("msg", "") + attempts = 0 + valid = False + while (attempts < CONTROL_RETRIES) and not valid: + attempts += 1 + result = await self._post_data_json(AT_READ, params, csrf=True) + if result[SUCCESS] is True: + jsondata = result[CONTENT] + if jsondata["code"] == "0": + _LOGGER.debug(f" cid: {str(cid):5s} - {jsondata.get('data',{}).get('msg','')}") + control_data[str(cid)] = jsondata.get("data", {}).get("msg", "") + valid = True + else: + error = f" cid: {str(cid):5s} - {AT_READ} responded with error: {jsondata['code']}:{jsondata['msg']}" + else: - _LOGGER.info( - f" cid: {str(cid):5s} - {AT_READ} responded with error: {jsondata['code']}:{jsondata['msg']}" - ) - else: - _LOGGER.info(f" cid: {str(cid):5s} - {AT_READ} responded with error: {result[MESSAGE]}") + error = f" cid: {str(cid):5s} - {AT_READ} responded with error: {result[MESSAGE]}" + + if not valid: + _LOGGER.info(error) return control_data @@ -765,11 +776,16 @@ async def write_control_data(self, device_serial: str, cid: str, value: str): if result[SUCCESS] is True: jsondata = result[CONTENT] if jsondata["code"] == "0": - _LOGGER.debug(f"Set code returned OK. Reading code back.") - await asyncio.sleep(CONTROL_DELAY) - control_data = await self.get_control_data(device_serial, cid=str(cid)) - _LOGGER.debug(f"Data read back: {control_data.get(str(cid), None)}") - + jsondata = jsondata["data"][0] + if jsondata["code"] == "0": + _LOGGER.debug(f"Set code returned OK. Reading code back.") + await asyncio.sleep(CONTROL_DELAY) + control_data = await self.get_control_data(device_serial, cid=str(cid)) + _LOGGER.debug(f"Data read back: {control_data.get(str(cid), None)}") + else: + _LOGGER.info( + f"cid: {str(cid):5s} - {CONTROL} responded with error: {jsondata['code']}:{jsondata.get('msg',None)}" + ) else: _LOGGER.info( f"cid: {str(cid):5s} - {CONTROL} responded with error: {jsondata['code']}:{jsondata.get('msg',None)}" diff --git a/custom_components/solis/time.py b/custom_components/solis/time.py index 41b1234..c889eaa 100644 --- a/custom_components/solis/time.py +++ b/custom_components/solis/time.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _LOGGER.debug(f"Domain: {DOMAIN}") service = hass.data[DOMAIN][config_entry.entry_id] - _LOGGER.info(f"Waiting for discovery of Timer entities for plant {plant_id}") + _LOGGER.info(f"Waiting for discovery of controls for plant {plant_id}") await asyncio.sleep(8) attempts = 0 while (attempts < RETRIES) and (not service.has_controls): @@ -41,30 +41,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn if service.has_controls: entities = [] _LOGGER.debug(f"Plant ID {plant_id} has controls:") - _LOGGER.debug(service.controls) for inverter_sn in service.controls: - _LOGGER.debug(f"Waiting for inverter {inverter_sn} HMI status") - attempts = 0 - while service.api._hmi_fb00[inverter_sn] is None: - _LOGGER.debug(f" Attempt {attempts} failed") - await asyncio.sleep(RETRY_WAIT) - attempts += 1 - hmi_fb00 = service.api._hmi_fb00[inverter_sn] - _LOGGER.debug(f"Inverter SN {inverter_sn} HMI status {hmi_fb00}") - for cid in service.controls[inverter_sn]: - for index, entity in enumerate(ALL_CONTROLS[hmi_fb00][cid]): - if isinstance(entity, SolisTimeEntityDescription): - _LOGGER.debug(f"Adding time entity {entity.name} for inverter Sn {inverter_sn} cid {cid}") - entities.append( - SolisTimeEntity( - service, - config_entry.data["name"], - inverter_sn, - cid, - entity, - index, - ) - ) + for cid, index, entity, button, initial_value in service.controls[inverter_sn]["time"]: + entities.append( + SolisTimeEntity( + service, + config_entry.data["name"], + inverter_sn, + cid, + entity, + index, + initial_value, + ) + ) if len(entities) > 0: _LOGGER.debug(f"Creating {len(entities)} time entities") @@ -79,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn class SolisTimeEntity(SolisBaseControlEntity, ServiceSubscriber, TimeEntity): - def __init__(self, service: InverterService, config_name, inverter_sn, cid, time_info, index): + def __init__(self, service: InverterService, config_name, inverter_sn, cid, time_info, index, initial_value): super().__init__(service, config_name, inverter_sn, cid, time_info) self._attr_native_value = datetime( year=YEAR, @@ -92,6 +81,8 @@ def __init__(self, service: InverterService, config_name, inverter_sn, cid, time self._splitter = time_info.splitter self._index = index # Subscribe to the service with the cid as the index + if initial_value is not None: + self.do_update(initial_value, datetime.now()) service.subscribe(self, inverter_sn, str(cid)) def do_update(self, value, last_updated): @@ -125,11 +116,11 @@ def do_update(self, value, last_updated): @property def to_string(self): - return self._attr_native_value.strftime('%H,%M') + return self._attr_native_value.strftime("%-H,%-M") async def async_set_value(self, value: float) -> None: _LOGGER.debug(f"async_set_value to {value} for {self._name}") self._attr_native_value = value self._attributes[LAST_UPDATED] = datetime.now() self.async_write_ha_state() - #await self.write_control_data(str(value)) + # await self.write_control_data(str(value))