Skip to content

Commit

Permalink
Merge pull request #26 from TimSoethout/#10-multiple-inverters+async
Browse files Browse the repository at this point in the history
#10 multiple inverters+async
  • Loading branch information
TimSoethout committed Apr 16, 2021
2 parents 9587f9c + c923072 commit afae1f4
Show file tree
Hide file tree
Showing 17 changed files with 533 additions and 187 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.pyc

.vscode
example.json

.DS_Store
99 changes: 59 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,91 +1,110 @@
# Goodwe SEMS integration for Home Assistant
# GoodWe SEMS API integration for Home Assistant

## Easiest install method via HACS
[![Paypal-shield]](https://www.paypal.com/donate?business=9NWEEX4P6998J&currency_code=EUR)
<a href="https://www.buymeacoffee.com/TimSoethout" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="20"></a>

Integration for Home Assistant that retrieves PV data from GoodWe SEMS API.

{% if prerelease %}
### NB!: This is a Beta version!
{% endif %}

## Setup

### Easiest install method via HACS

[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs)

The repository folder structure is compatible with [HACS](https://hacs.xyz) and is included by default in HACS.

Install HACS via: https://hacs.xyz/docs/installation/manual.
Then search for "SEMS" in the Integrations tab (under Community).

## Setup
### Manual Setup

Crude sensor for Home Assistant that scrapes from GoodWe SEMS portal. Copy all the files in `custom_components/sems/` to `custom_components/sems/` your Home Assistant config dir.

Crude sensor for Home Assistant that scrapes from GoodWe SEMS portal. Copy all the files in `custom_components/sems/` in your Home Assistant config dir:
- `sensor.py`
- `__init__.py`
- `manifest.json`
## Configure integration

And update configuration. The ID of your Power Station can be retrieved by logging in to the SEMS Portal with your credentials:
The required ID of your Power Station can be retrieved by logging in to the SEMS Portal with your credentials:
https://www.semsportal.com

After login you'll see the ID in your URL. E.g.:
https://www.semsportal.com/powerstation/powerstatussnmin/12345678-1234-1234-1234-123456789012
After login you'll see the ID in your URL, e.g.:
https://semsportal.com/PowerStation/PowerStatusSnMin/12345678-1234-1234-1234-123456789012

In this example the ID of the Power Station is: 12345678-1234-1234-1234-123456789012

Example entry in `configuration.yaml`:
In the home assistant GUI, go to `Configuration` > `Integrations` and click the `Add Integration` button. Search for `GoodWe SEMS API`.

```
sensor:
- platform: sems
username: 'XXXX'
password: 'XXXX'
station_id : '12345678-1234-1234-1234-123456789012'
scan_interval: 60
# Optional/example
# A template to ease access to the data as "sensor.pv_outputpower" etc.
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.


### Extra (optional) templates to easy access data as sensors
Replace `$NAME` with your inverter name.
```yaml
- platform: template
sensors:
pv_outputpower:
value_template: '{{ states.sensor.sems_portal.attributes.outputpower }}'
value_template: '{{ states.sensor.inverter_$NAME.attributes.outputpower }}'
unit_of_measurement: 'W'
friendly_name: "PV Power output"
pv_temperature:
value_template: '{{ states.sensor.sems_portal.attributes.tempperature }}'
value_template: '{{ states.sensor.inverter_$NAME.attributes.tempperature }}'
unit_of_measurement: 'C'
friendly_name: "PV Temperature"
pv_eday:
value_template: '{{ states.sensor.sems_portal.attributes.eday }}'
value_template: '{{ states.sensor.inverter_$NAME.attributes.eday }}'
unit_of_measurement: 'kWh'
friendly_name: "PV energy day"
pv_etotal:
value_template: '{{ states.sensor.sems_portal.attributes.etotal }}'
value_template: '{{ states.sensor.inverter_$NAME.attributes.etotal }}'
unit_of_measurement: 'kWh'
friendly_name: "PV energy total"
pv_iday:
value_template: '{{ states.sensor.sems_portal.attributes.iday }}'
value_template: '{{ states.sensor.inverter_$NAME.attributes.iday }}'
unit_of_measurement: ''
friendly_name: "PV income day"
pv_itotal:
value_template: '{{ states.sensor.sems_portal.attributes.itotal }}'
value_template: '{{ states.sensor.inverter_$NAME.attributes.itotal }}'
unit_of_measurement: ''
friendly_name: "PV income total"
pv_excess:
value_template: '{{ states.sensor.sems_portal.attributes.pmeter }}'
value_template: '{{ states.sensor.inverter_$NAME.attributes.pmeter }}'
unit_of_measurement: 'W'
friendly_name: "PV spare"
# battery soc
pv_soc:
value_template: '{{ states.sensor.sems_portal.attributes.soc }}'
value_template: '{{ states.sensor.inverter_$NAME.attributes.soc }}'
unit_of_measurement: '%'
friendly_name: "Battery power"
# PV output power only
pv_outputpower:
value_template: '{{ states.sensor.sems_portal.attributes.outputpower }}'
unit_of_measurement: 'W'
friendly_name: "PV Power output"
```

Use the credentials you use to login to https://www.semsportal.com/.
## Screenies

`scan_interval` controls how often the sensor updates/scrapes. By default this seems to be every 60 seconds.
![Detail window](images/sems-details.webp)

## Screenies
![Detail window](images/search-integration.webp)

![Detail window](images/integration-flow.webp)

## Debug info

Add the last line in `configuration.yaml` in the relevant part of `logger`:

```yaml
logger:
default: info
logs:
custom_components.sems: debug
```

![Overview icon](images/sems-icon.png)
## Notes

![Detail window](images/sems-details.png)
* Sometimes the SEMS API is a bit slow, so time-out messages may occur in the log as `[ERROR]`. The component should continue to work normally and try fetch again the next minute.

## Credits

Reuses code from https://github.com/Sprk-nl/goodwe_sems_portal_scraper.
Inspired by https://github.com/Sprk-nl/goodwe_sems_portal_scraper and https://github.com/bouwew/sems2mqtt .
Also supported by generous contributions by various helpful community members.
53 changes: 53 additions & 0 deletions custom_components/sems/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,54 @@
"""The sems integration."""
from __future__ import annotations

import asyncio

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from .const import DOMAIN
from .sems_api import SemsApi

# TODO List the platforms that you want to support.
# For your initial PR, limit it to 1 platform.
PLATFORMS = ["sensor"]


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the sems component."""
# Ensure our name space for storing objects is a known type. A dict is
# common/preferred as it allows a separate instance of your class for each
# instance that has been created in the UI.
hass.data.setdefault(DOMAIN, {})

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up sems from a config entry."""
hass.data[DOMAIN][entry.entry_id] = SemsApi(
hass, entry.data["username"], entry.data["password"]
)

for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
86 changes: 86 additions & 0 deletions custom_components/sems/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Config flow for sems integration."""
from __future__ import annotations

import logging
from typing import Any

from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError

from .const import DOMAIN, SEMS_CONFIG_SCHEMA
from .sems_api import SemsApi

_LOGGER = logging.getLogger(__name__)


async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
# TODO validate the data can be used to set up a connection.

# If your PyPI package is not built with async, pass your methods
# to the executor:
# await hass.async_add_executor_job(
# your_validate_func, data["username"], data["password"]
# )

api = SemsApi(hass, data["username"], data["password"])

authenticated = await hass.async_add_executor_job(api.test_authentication)
if not authenticated:
raise InvalidAuth

# If you cannot connect:
# throw CannotConnect
# If the authentication is wrong:
# InvalidAuth

# Return info that you want to store in the config entry.
return {
"powerstation_id": data["powerstation_id"],
"username": data["username"],
"password": data["password"],
}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for sems."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> dict[str, Any]:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(step_id="user", data_schema=SEMS_CONFIG_SCHEMA)

errors = {}

try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(title=info["powerstation_id"], data=user_input)

return self.async_show_form(
step_id="user", data_schema=SEMS_CONFIG_SCHEMA, errors=errors
)


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
22 changes: 22 additions & 0 deletions custom_components/sems/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Constants for the sems integration."""

DOMAIN = "sems"

import voluptuous as vol
from homeassistant.const import (
CONF_PASSWORD,
CONF_USERNAME,
DEVICE_CLASS_POWER,
POWER_WATT,
)

CONF_STATION_ID = "powerstation_id"

# Validation of the user's configuration
SEMS_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_STATION_ID): str,
}
)
16 changes: 9 additions & 7 deletions custom_components/sems/manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{
"domain": "sems",
"name": "Goodwe SEMS API",
"documentation": "https://github.com/TimSoethout/goodwe-sems-home-assistant",
"dependencies": [],
"codeowners": ["@TimSoethout"],
"domain": "sems",
"name": "GoodWe SEMS API",
"config_flow": true,
"documentation": "https://github.com/TimSoethout/goodwe-sems-home-assistant",
"issue_tracker": "https://github.com/TimSoethout/goodwe-sems-home-assistant/issues",
"requirements": [],
"version": "1.0.1"
}
"dependencies": [],
"codeowners": ["@TimSoethout"],
"version": "3.1.0-beta"
}

0 comments on commit afae1f4

Please sign in to comment.