diff --git a/.gitignore b/.gitignore index 1dd07b4..9080399 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ custom_components/solis/__pycache__/* +.vscode/* diff --git a/custom_components/solis/button.py b/custom_components/solis/button.py index e71a958..45bf58f 100644 --- a/custom_components/solis/button.py +++ b/custom_components/solis/button.py @@ -17,7 +17,6 @@ from .service import InverterService, ServiceSubscriber _LOGGER = logging.getLogger(__name__) -RETRIES = 100 async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities): @@ -29,13 +28,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn service = hass.data[DOMAIN][config_entry.entry_id] _LOGGER.info(f"Waiting for discovery of controls for plant {plant_id}") - await asyncio.sleep(8) + await asyncio.sleep(RETRY_WAIT) attempts = 0 while (attempts < RETRIES) and (not service.has_controls): _LOGGER.debug(f" Attempt {attempts} failed") await asyncio.sleep(RETRY_WAIT) attempts += 1 + _LOGGER.debug(f" Attempt {attempts} succeeded") + if service.has_controls: entities = [] _LOGGER.debug(f"Plant ID {plant_id} has controls:") diff --git a/custom_components/solis/config_flow.py b/custom_components/solis/config_flow.py index 7e2fd6c..8c5d299 100644 --- a/custom_components/solis/config_flow.py +++ b/custom_components/solis/config_flow.py @@ -53,19 +53,26 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Flo errors = {} if user_input is not None: - updated_config = {} - for key in self.config_entry.data.keys(): - updated_config[key] = self.config_entry.data[key] - updated_config[CONF_CONTROL] = False - for key in (CONF_CONTROL, CONF_PASSWORD, CONF_REFRESH_OK, CONF_REFRESH_NOK): - if key in user_input: - updated_config[key] = user_input[key] + updated_config = dict(self.config_entry.data) + + control_section = user_input.get("Control") or {} + new_pw = control_section.get(CONF_PASSWORD) + + if new_pw: # only overwrite if user actually typed something + updated_config[CONF_PASSWORD] = new_pw + + updated_config[CONF_CONTROL] = control_section.get(CONF_CONTROL, updated_config.get(CONF_CONTROL, False)) + updated_config[CONF_REFRESH_OK] = user_input.get(CONF_REFRESH_OK, updated_config.get(CONF_REFRESH_OK, 300)) + updated_config[CONF_REFRESH_NOK] = user_input.get( + CONF_REFRESH_NOK, updated_config.get(CONF_REFRESH_NOK, 60) + ) self.hass.config_entries.async_update_entry( self.config_entry, data=updated_config, - title=user_input.get(CONF_NAME), + title=self.config_entry.title, ) + data_schema = { vol.Required(CONF_REFRESH_OK, default=300): cv.positive_int, vol.Required(CONF_REFRESH_NOK, default=60): cv.positive_int, @@ -115,12 +122,12 @@ async def async_step_user(self, user_input=None): errors: dict[str, str] = {} if user_input is not None: - self._data = user_input + self._data = dict(user_input) if user_input.get(CONF_PORTAL_VERSION) is None: - user_input[CONF_PORTAL_VERSION] = PLATFORMV2 - if user_input.get(CONF_PORTAL_VERSION) == PLATFORMV2: - return await self.async_step_credentials_password(user_input) - return await self.async_step_credentials_secret(user_input) + self._data[CONF_PORTAL_VERSION] = PLATFORMV2 + if self._data[CONF_PORTAL_VERSION] == PLATFORMV2: + return await self.async_step_credentials_password() # no arg + return await self.async_step_credentials_secret() # no arg data_schema = { vol.Required(CONF_NAME, default=SENSOR_PREFIX): cv.string, @@ -160,7 +167,7 @@ async def async_step_credentials_password(self, user_input=None): data_schema = { vol.Required(CONF_USERNAME, default=None): cv.string, - vol.Required(CONF_PASSWORD, default=""): cv.string, + vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PLANT_ID, default=None): cv.positive_int, } @@ -171,27 +178,32 @@ async def async_step_credentials_password(self, user_input=None): ) async def async_step_credentials_secret(self, user_input=None): - """Handle key_id/secret based credential settings.""" errors: dict[str, str] = {} if user_input is not None: + control_section = user_input.get("Control") or {} + password = control_section.get(CONF_PASSWORD) or user_input.get(CONF_PASSWORD) + url = self._data.get(CONF_PORTAL_DOMAIN) plant_id = user_input.get(CONF_PLANT_ID) username = user_input.get(CONF_USERNAME) - password = user_input.get(CONF_PASSWORD) key_id = user_input.get(CONF_KEY_ID) - secret: bytes = bytes("", "utf-8") - schedule_ok = user_input.get(CONF_REFRESH_OK) - schedule_nok = user_input.get(CONF_REFRESH_NOK) + + # SECRET comes from top-level field try: secret = bytes(user_input.get(CONF_SECRET), "utf-8") except TypeError: - pass + secret = b"" + if url[:8] != "https://": errors["base"] = "invalid_path" else: if username and key_id and secret and plant_id: - self._data.update(user_input) + # Merge nested section keys into _data so CONF_PASSWORD is stored + merged = dict(user_input) + merged.update(control_section) # brings CONF_PASSWORD, CONF_CONTROL to top + self._data.update(merged) + config = SoliscloudConfig(url, username, key_id, secret, plant_id, password) api = SoliscloudAPI(config) if await api.login(async_get_clientsession(self.hass)): @@ -210,10 +222,9 @@ async def async_step_credentials_secret(self, user_input=None): vol.Schema( { vol.Required(CONF_CONTROL, default=False): bool, - vol.Optional(CONF_PASSWORD, default=""): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, } ), - # Whether or not the section is initially collapsed (default = False) {"collapsed": False}, ), } diff --git a/custom_components/solis/control_const.py b/custom_components/solis/control_const.py index 217a2b1..d5923b1 100644 --- a/custom_components/solis/control_const.py +++ b/custom_components/solis/control_const.py @@ -12,13 +12,15 @@ from .const import API_NAME, DOMAIN, EMPTY_ATTR, SERIAL -RETRIES = 1000 +RETRIES = 100 RETRY_WAIT = 10 _LOGGER = logging.getLogger(__name__) class SolisBaseControlEntity: + _attr_entity_registry_enabled_default = True + def __init__(self, service, config_name, inverter_sn, cid, info): self._measured: datetime | None = None self._entity_type = "control" @@ -37,7 +39,7 @@ def __init__(self, service, config_name, inverter_sn, cid, info): @property def unique_id(self) -> str: - return f"{self._platform_name}_{self._key}" + return f"{self._platform_name}_{self._inverter_sn}_{self._key}" @property def name(self) -> str: @@ -58,10 +60,10 @@ def device_info(self) -> DeviceInfo | None: identifiers={ ( DOMAIN, - f"{self._attributes[SERIAL]}_{DOMAIN, self._attributes[API_NAME]}", + f"{self._attributes[SERIAL]}_{self._attributes[API_NAME]}", ) }, - manufacturer=f"Solis {self._attributes[API_NAME]}", + manufacturer=f"Solis", name=f"Solis_Inverter_{self._attributes[SERIAL]}", ) @@ -1070,19 +1072,30 @@ class SolisButtonEntityDescription(ButtonEntityDescription): name="Energy Storage Control Switch", key="energy_storage_control_switch", option_dict={ - 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", + "35": "Self-Use - Allow Timed Charge/Discharge, Allow Grid Charging", + "3": "Self-Use - Allow Timed Charge/Discharge, No Grid Charging", + "33": "Self-Use - No Timed Charge/Discharge, Allow Grid Charging", + "1": "Self-Use - No Timed Charge/Discharge, No Grid Charging", + + "51": "Self-Use with Backup - Allow Timed Charge/Discharge, Allow Grid Charging", + "19": "Self-Use with Backup - Allow Timed Charge/Discharge, No Grid Charging", + "49": "Self-Use with Backup - No Timed Charge/Discharge, Allow Grid Charging", + "17": "Self-Use with Backup - No Timed Charge/Discharge, No Grid Charging", + + "43": "Battery Awaken - Allow Timed Charge/Discharge", + "41": "Battery Awaken - No Timed Charge/Discharge", + + "98": "Feed-in Priority - Allow Timed Charge/Discharge, Allow Grid Charging", + "66": "Feed-in Priority - Allow Timed Charge/Discharge, No Grid Charging", + "96": "Feed-in Priority - No Timed Charge/Discharge, Allow Grid Charging", + "64": "Feed-in Priority - No Timed Charge/Discharge, No Grid Charging", + + "114": "Feed-in Priority with Backup - Allow Timed Charge/Discharge, Allow Grid Charging", + "82": "Feed-in Priority with Backup - Allow Timed Charge/Discharge, No Grid Charging", + "112": "Feed-in Priority with Backup - No Timed Charge/Discharge, Allow Grid Charging", + "80": "Feed-in Priority with Backup - No Timed Charge/Discharge, No Grid Charging", + + "37": "Off-Grid", }, icon="mdi:dip-switch", ) diff --git a/custom_components/solis/number.py b/custom_components/solis/number.py index d016b73..9398b4c 100644 --- a/custom_components/solis/number.py +++ b/custom_components/solis/number.py @@ -18,7 +18,7 @@ from .service import InverterService, ServiceSubscriber _LOGGER = logging.getLogger(__name__) -RETRIES = 100 +# RETRIES = 100 async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities): @@ -30,13 +30,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn service = hass.data[DOMAIN][config_entry.entry_id] _LOGGER.info(f"Waiting for discovery of controls for plant {plant_id}") - await asyncio.sleep(8) + await asyncio.sleep(RETRY_WAIT) attempts = 0 while (attempts < RETRIES) and (not service.has_controls): _LOGGER.debug(f" Attempt {attempts} failed") await asyncio.sleep(RETRY_WAIT) attempts += 1 + _LOGGER.debug(f" Attempt {attempts} succeeded") + if service.has_controls: entities = [] _LOGGER.debug(f"Plant ID {plant_id} has controls:") diff --git a/custom_components/solis/select.py b/custom_components/solis/select.py index 547504c..c6dc5b4 100644 --- a/custom_components/solis/select.py +++ b/custom_components/solis/select.py @@ -27,13 +27,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn service = hass.data[DOMAIN][config_entry.entry_id] _LOGGER.info(f"Waiting for discovery of controls for plant {plant_id}") - await asyncio.sleep(8) + await asyncio.sleep(RETRY_WAIT) attempts = 0 while (attempts < RETRIES) and (not service.has_controls): _LOGGER.debug(f" Attempt {attempts} failed") await asyncio.sleep(RETRY_WAIT) attempts += 1 + _LOGGER.debug(f" Attempt {attempts} succeeded") + if service.has_controls: entities = [] _LOGGER.debug(f"Plant ID {plant_id} has controls:") diff --git a/custom_components/solis/sensor.py b/custom_components/solis/sensor.py index 4818f5c..ae46392 100644 --- a/custom_components/solis/sensor.py +++ b/custom_components/solis/sensor.py @@ -163,6 +163,9 @@ def on_discovered(capabilities, cookie): class SolisSensor(ServiceSubscriber, SensorEntity): """Representation of a Solis sensor.""" + _attr_entity_registry_enabled_default = True + _attr_has_entity_name = True + def __init__( self, ginlong_service: InverterService, @@ -224,10 +227,10 @@ def device_info(self) -> DeviceInfo | None: identifiers={ ( DOMAIN, - f"{self._attributes[SERIAL]}_{DOMAIN, self._attributes[API_NAME]}", + f"{self._attributes[SERIAL]}_{self._attributes[API_NAME]}", ) }, - manufacturer=f"Solis {self._attributes[API_NAME]}", + manufacturer=f"Solis", name=f"Solis_Inverter_{self._attributes[SERIAL]}", # model=config.modelid, # sw_version=config.swversion, diff --git a/custom_components/solis/soliscloud_api.py b/custom_components/solis/soliscloud_api.py index b9525f6..ab1c0d1 100644 --- a/custom_components/solis/soliscloud_api.py +++ b/custom_components/solis/soliscloud_api.py @@ -391,14 +391,14 @@ async def fetch_inverter_data(self, inverter_serial: str, controls=True) -> Ginl self._collect_inverter_data(payload) if inverter_serial not in self._hmi_fb00: hmi_flag = self._data[HMI_VERSION_ALL] - self._hmi_fb00[inverter_serial] = int(hmi_flag, 16) >= int("4200", 16) + self._hmi_fb00[inverter_serial] = int(hmi_flag, 16) >= int("4b00", 16) if self._hmi_fb00[inverter_serial]: _LOGGER.debug( - f"HMI firmware version ({hmi_flag}) >=4200 for Inverter SN {inverter_serial} " + f"HMI firmware version ({hmi_flag}) >=4B00 for Inverter SN {inverter_serial} " ) else: _LOGGER.debug( - f"HMI firmware version ({hmi_flag}) <4200 for Inverter SN {inverter_serial} " + f"HMI firmware version ({hmi_flag}) <4B00 for Inverter SN {inverter_serial} " ) if (self._token != "") and controls: @@ -792,8 +792,7 @@ async def _fetch_token(self, username: str, password: str) -> str: return "" async def write_control_data(self, device_serial: str, cid: str, value: str): - _LOGGER.debug(f">>> Writing value {value} for cid {cid} to inverter {device_serial}") - + _LOGGER.debug(f"Writing value {value} for cid {cid} to inverter {device_serial}") params = {"inverterSn": str(device_serial), "cid": str(cid), "value": value} result = await self._post_data_json(CONTROL, params, csrf=True) diff --git a/custom_components/solis/time.py b/custom_components/solis/time.py index 2fe9e63..e888c53 100644 --- a/custom_components/solis/time.py +++ b/custom_components/solis/time.py @@ -17,7 +17,6 @@ from .service import InverterService, ServiceSubscriber _LOGGER = logging.getLogger(__name__) -RETRIES = 100 YEAR = 2024 MONTH = 1 DAY = 1 @@ -32,13 +31,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn service = hass.data[DOMAIN][config_entry.entry_id] _LOGGER.info(f"Waiting for discovery of controls for plant {plant_id}") - await asyncio.sleep(8) + await asyncio.sleep(RETRY_WAIT) attempts = 0 while (attempts < RETRIES) and (not service.has_controls): _LOGGER.debug(f" Attempt {attempts} failed") await asyncio.sleep(RETRY_WAIT) attempts += 1 + _LOGGER.debug(f" Attempt {attempts} succeeded") + if service.has_controls: entities = [] _LOGGER.debug(f"Plant ID {plant_id} has controls:")