Skip to content

Commit

Permalink
Merge pull request #103 from cthulu/adding_power_control
Browse files Browse the repository at this point in the history
Adding power control
  • Loading branch information
TimSoethout authored Mar 20, 2024
2 parents 7f66ef6 + 884f521 commit 6cb0270
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 6 deletions.
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion custom_components/sems/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion custom_components/sems/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"requirements": [],
"dependencies": [],
"codeowners": ["@TimSoethout"],
"version": "4.0.0"
"version": "5.0.0"
}
54 changes: 54 additions & 0 deletions custom_components/sems/sems_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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."""
3 changes: 2 additions & 1 deletion custom_components/sems/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__)
Expand Down Expand Up @@ -569,3 +569,4 @@ async def async_update(self):
Only used by the generic entity update service.
"""
await self.coordinator.async_request_refresh()

75 changes: 75 additions & 0 deletions custom_components/sems/switch.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 6cb0270

Please sign in to comment.