diff --git a/README.md b/README.md index f13f9ab..1187217 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,18 @@ Fill in the required configuration and it should find your inverters. Note that changed to `configuration.yaml` are no longer necessary and can be removed. -### Recommended: use visitor account to login with this integration +### Optional: control the invertor power output via the "switch" entity + +It is possible to temporarily pause the energy production via "downtime" functionality available on the invertor. This is exposed as a switch and can be used in your own automations. + +Please note that it is using an undocumented API and can take a few minutes for the invertor to pick up the change. It takes approx 60 seconds to start again when the invertor is in a downtime mode. + +### Recommended: use visitor account if you do not need to control the inverter + +In case you are only reading the inverter stats, you can use a Visitor (read-only) account. Create via the official app, or via the web portal: -Login to www.semsportal.com, go to https://semsportal.com/powerstation/stationInfonew. Create a new visitor account. +Login to www.semsportal.com, go to https://semsportal.com/powerstation/stationInfonew. Create a new visitor account. Login to the visitor account once to accept the EULA. Now you should be able to use it in this component. ### Extra (optional) templates to easy access data as sensors @@ -78,7 +86,7 @@ Replace `$NAME` with your inverter entity id. friendly_name: "Battery power" ``` -Note that `states.sensor.inverter_$NAME.state` contains the power output in `W`. +Note that `states.sensor.inverter_$NAME.state` contains the power output in `W`. ## Screenies diff --git a/custom_components/sems/__init__.py b/custom_components/sems/__init__.py index 5c829e0..0746c28 100644 --- a/custom_components/sems/__init__.py +++ b/custom_components/sems/__init__.py @@ -14,7 +14,7 @@ from .sems_api import SemsApi # For your initial PR, limit it to 1 platform. -PLATFORMS = ["sensor"] +PLATFORMS = ["sensor", "switch"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/custom_components/sems/manifest.json b/custom_components/sems/manifest.json index e2a18ed..0c8c488 100644 --- a/custom_components/sems/manifest.json +++ b/custom_components/sems/manifest.json @@ -8,5 +8,5 @@ "requirements": [], "dependencies": [], "codeowners": ["@TimSoethout"], - "version": "4.0.0" + "version": "5.0.0" } diff --git a/custom_components/sems/sems_api.py b/custom_components/sems/sems_api.py index e181c95..2b0fc70 100644 --- a/custom_components/sems/sems_api.py +++ b/custom_components/sems/sems_api.py @@ -10,6 +10,7 @@ # _LoginURL = "https://eu.semsportal.com/api/v2/Common/CrossLogin" _LoginURL = "https://www.semsportal.com/api/v2/Common/CrossLogin" _PowerStationURLPart = "/v2/PowerStation/GetMonitorDetailByPowerstationId" +_PowerControlURL = "https://www.semsportal.com/api/PowerStation/SaveRemoteControlInverter" _RequestTimeout = 30 # seconds _DefaultHeaders = { @@ -128,6 +129,59 @@ def getData(self, powerStationId, renewToken=False, maxTokenRetries=2): except Exception as exception: _LOGGER.error("Unable to fetch data from SEMS. %s", exception) + def change_status(self, inverterSn, status, renewToken=False, maxTokenRetries=2): + """Schedule the downtime of the station""" + try: + # Get the status of our SEMS Power Station + _LOGGER.debug("SEMS - Making Power Station Status API Call") + if maxTokenRetries <= 0: + _LOGGER.info( + "SEMS - Maximum token fetch tries reached, aborting for now" + ) + raise OutOfRetries + if self._token is None or renewToken: + _LOGGER.debug( + "API token not set (%s) or new token requested (%s), fetching", + self._token, + renewToken, + ) + self._token = self.getLoginToken(self._username, self._password) + + # Prepare Power Station status Headers + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "token": json.dumps(self._token), + } + + powerControlURL = _PowerControlURL + _LOGGER.debug( + "Sending power control command (%s) for power station id: %s", + powerControlURL, + inverterSn, + ) + + data = { + "InverterSN": inverterSn, + "InverterStatusSettingMark":"1", + "InverterStatus": str(status) + } + + response = requests.post( + powerControlURL, headers=headers, json=data, timeout=_RequestTimeout + ) + if (response.status_code != 200): + # try again and renew token is unsuccessful + _LOGGER.warn( + "Power control command not successful, retrying with new token, %s retries remaining", + maxTokenRetries, + ) + return + + return + except Exception as exception: + _LOGGER.error("Unable to execute Power control command. %s", exception) + class OutOfRetries(exceptions.HomeAssistantError): """Error to indicate too many error attempts.""" diff --git a/custom_components/sems/sensor.py b/custom_components/sems/sensor.py index 4419b79..924afe1 100644 --- a/custom_components/sems/sensor.py +++ b/custom_components/sems/sensor.py @@ -5,6 +5,7 @@ https://github.com/TimSoethout/goodwe-sems-home-assistant """ +from typing import Coroutine from homeassistant.core import HomeAssistant import homeassistant import logging @@ -26,7 +27,6 @@ UnitOfPower, UnitOfEnergy, ) -from homeassistant.helpers.entity import Entity from .const import DOMAIN, CONF_STATION_ID, DEFAULT_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -569,3 +569,4 @@ async def async_update(self): Only used by the generic entity update service. """ await self.coordinator.async_request_refresh() + diff --git a/custom_components/sems/switch.py b/custom_components/sems/switch.py new file mode 100644 index 0000000..e1c6203 --- /dev/null +++ b/custom_components/sems/switch.py @@ -0,0 +1,75 @@ +""" +Support for switch controlling an output of a GoodWe SEMS inverter. + +For more details about this platform, please refer to the documentation at +https://github.com/TimSoethout/goodwe-sems-home-assistant +""" + +import logging + +from homeassistant.const import ( + CONF_SCAN_INTERVAL, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import UpdateFailed +from .const import DOMAIN, CONF_STATION_ID +from homeassistant.components.switch import SwitchEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add switches for passed config_entry in HA.""" + semsApi = hass.data[DOMAIN][config_entry.entry_id] + stationId = config_entry.data[CONF_STATION_ID] + + try: + result = await hass.async_add_executor_job(semsApi.getData, stationId) + + except Exception as err: + # logging.exception("Something awful happened!") + raise UpdateFailed(f"Error communicating with API: {err}") + + inverters = result["inverter"] + entities = [] + for inverter in inverters: + entities.append(SemsSwitch(semsApi, inverter["invert_full"]["sn"])) + + async_add_entities(entities) + +class SemsSwitch(SwitchEntity): + + def __init__(self, api, sn): + super().__init__() + self.api = api + self.sn = sn + _LOGGER.debug(f"Creating SemsSwitch for Inverter {self.sn}") + + @property + def name(self) -> str: + """Return the name of the switch.""" + return f"Inverter {self.sn} Switch" + + + @property + def unique_id(self) -> str: + return f"{self.sn}-switch" + + @property + def device_info(self): + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.sn) + }, + "name": "Homekit", + "manufacturer": "GoodWe", + } + + def async_turn_off(self, **kwargs): + _LOGGER.debug(f"Inverter {self.sn} set to Off") + self.api.change_status(self.sn, 2) + + def async_turn_on(self, **kwargs): + _LOGGER.debug(f"Inverter {self.sn} set to On") + self.api.change_status(self.sn, 4)