Compare commits
2 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
ee7be37ba6 | |
|
|
8034ada12f |
|
|
@ -1,21 +0,0 @@
|
||||||
name: Build & Attach ZIP asset
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-zip:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Create ZIP
|
|
||||||
run: |
|
|
||||||
mkdir -p dist
|
|
||||||
cd custom_components/sws12500
|
|
||||||
zip -r ../../dist/weather-station.zip . -x "*/__pycache__/*"
|
|
||||||
|
|
||||||
- name: Upload ZIP to release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: dist/weather-station.zip
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
* @schizza
|
|
||||||
167
README.md
167
README.md
|
|
@ -1,127 +1,33 @@
|
||||||

|
# Integrates your Sencor SWS 12500 or 16600, GARNI, BRESSER weather stations seamlessly into Home Assistant
|
||||||

|
|
||||||
|
|
||||||
# Integrates your Sencor SWS 12500, SWS16600, SWS 10500, GARNI, BRESSER weather stations seamlessly into Home Assistant
|
This integration will listen for data from your station and passes them to respective sensors. It also provides the ability to push data to Windy API.
|
||||||
|
|
||||||
This integration will listen for data from your station and passes them to respective sensors. It also provides the ability to push data to `Windy API` or `Pocasi Meteo`.
|
_This custom component replaces [old integration via Node-RED and proxy server](https://github.com/schizza/WeatherStation-SWS12500)._
|
||||||
|
|
||||||
### Ecowitt support is coming in the next major release
|
## Warning - WSLink APP (applies also for SWS 12500 with firmware >3.0)
|
||||||
|
|
||||||
As of April 11, 2026, Ecowitt stations are supported in the pre-release version
|
For stations that are using WSLink app to setup station and WSLink API for resending data (SWS 12500 manufactured in 2024 and later). You will need to install [WSLink SSL proxy addon](https://github.com/schizza/wslink-addon) to your Home Assistant if you are not running your Home Assistant instance in SSL mode or you do not have SSL proxy for your Home Assistant.
|
||||||
[v2.0.0pre1](https://github.com/schizza/SWS-12500-custom-component/releases/tag/v2.0.0pre1).
|
|
||||||
You can download this pre-release in HACS under `target version`, where you can pick the exact
|
|
||||||
version of the integration. Please be aware that this pre-release is really for testing
|
|
||||||
purposes only.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Integration rename is planned for the next major release
|
|
||||||
|
|
||||||
The current name no longer reflects what the integration does. It was initially developed
|
|
||||||
primarily for the SWS 12500 station, but it already supports other weather stations as well
|
|
||||||
(Bresser, Garni and others), and Ecowitt support is on the way — so the current name has
|
|
||||||
become misleading. The transition will be announced via an update, and I'm also planning to
|
|
||||||
offer a full data migration from the existing integration to the new one, so you won't lose
|
|
||||||
any of your historical data.
|
|
||||||
|
|
||||||
- The transition date hasn't been set yet, but it's currently expected to happen within the
|
|
||||||
next ~2–3 months. At the moment, I'm working on a full refactor and general code cleanup.
|
|
||||||
Looking further ahead, the goal is to have the integration fully incorporated into Home
|
|
||||||
Assistant as a native component — meaning it won't need to be installed via HACS but will
|
|
||||||
become part of the official Home Assistant distribution.
|
|
||||||
|
|
||||||
- I'm also looking for someone who owns an Ecowitt weather station and would be willing to
|
|
||||||
help with testing the integration for these devices.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Warning — WSLink app (applies also to SWS 12500 with firmware > 3.0)
|
|
||||||
|
|
||||||
Please read the **IMPORTANT** note below.
|
|
||||||
|
|
||||||
For stations that use the WSLink app to set up the station and the WSLink API for resending
|
|
||||||
data (also SWS 12500 stations manufactured in 2024 or later), you will need to install the
|
|
||||||
[WSLink SSL proxy add-on](https://github.com/schizza/wslink-addon) into your Home Assistant
|
|
||||||
— unless you are already running Home Assistant in SSL mode or you have your own SSL proxy
|
|
||||||
in front of Home Assistant.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> The recommendation above does not apply to all stations. As is known by now, some stations
|
|
||||||
> — even when configured via the `WSLink App` — do not use the `WSLink protocol` to send
|
|
||||||
> data to custom servers. It can therefore be confusing how to configure your station, and
|
|
||||||
> for that case I made a simple debugging server (see below).
|
|
||||||
|
|
||||||
## Not Sure How Your Station Sends Data?
|
|
||||||
|
|
||||||
If you are struggling with configuration and don't know which protocol your station uses (`WSLink` or `PWS/WU`), or whether it sends data over SSL or plain HTTP — use our public test server:
|
|
||||||
|
|
||||||
**<https://test-station.schizza.cz>**
|
|
||||||
|
|
||||||
1. Open the link above and create a new session.
|
|
||||||
2. Point your weather station to the generated subdomain address.
|
|
||||||
3. Within a few seconds the server will show you:
|
|
||||||
- the **protocol** your station is using (`WSLink` or `PWS/WU`)
|
|
||||||
- whether the request arrived over **SSL** or **non-SSL**
|
|
||||||
- the full query string, headers, and payload your station sent
|
|
||||||
|
|
||||||
This information tells you exactly how to configure the integration in Home Assistant.
|
|
||||||
|
|
||||||
### Quick Recommendations
|
|
||||||
|
|
||||||
| Station sends via | Protocol | Recommended Setup |
|
|
||||||
|---|---|--- |
|
|
||||||
| **plain HTTP** | PWS/WU | Point the station directly at Home Assistant — no proxy needed. Keep `WSLink API` unchecked (disabled). |
|
|
||||||
| **plain HTTP** | WSLink | Point the station directly at Home Assistant — no proxy needed. Enable `WSLink API` in settings. |
|
|
||||||
| **HTTPS (SSL)** | PWS/WU | Install the [WSLink Proxy Add-on](https://github.com/schizza/wslink-addon) — it terminates TLS and forwards plain HTTP to Home Assistant. Keep `WSLink API` unchecked (disabled). |
|
|
||||||
| **HTTPS (SSL)** | WSLink | Install the [WSLink Proxy Add-on](https://github.com/schizza/wslink-addon) — it terminates TLS and forwards plain HTTP to Home Assistant, and enable `WSLink API`. |
|
|
||||||
|
|
||||||
— If the test server shows your station is sending over SSL, you need the [WSLink Proxy Add-on](https://github.com/schizza/wslink-addon). If it sends plain HTTP, you can connect directly.
|
|
||||||
|
|
||||||
Web server repo is reachable here: <https://github.com/schizza/test-station-server>
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Weather station that supports sending data to custom server in their API [(list of supported stations.)](#list-of-supported-stations)
|
- Weather station that supports sending data to custom server in their API [(list of supported stations.)](#list-of-supported-stations)
|
||||||
- Configure station to send data directly to Home Assistant.
|
- Configure station to send data directly to Home Assistant.
|
||||||
- If you want to push data to Windy, you have to create an account at [Windy](https://stations.windy.com).
|
- If you want to push data to Windy, you have to create an account at [Windy](https://stations.windy.com).
|
||||||
- If you want to resend data to `Pocasi Meteo`, you have to create accout at [Pocasi Meteo](https://pocasimeteo.cz)
|
|
||||||
|
|
||||||
## Examples of supported stations
|
## List of supported stations
|
||||||
|
|
||||||
- [Sencor SWS 12500 Weather Station](https://www.sencor.cz/profesionalni-meteorologicka-stanice/sws-12500)
|
- [Sencor SWS 12500 Weather Station](https://www.sencor.cz/profesionalni-meteorologicka-stanice/sws-12500)
|
||||||
- [Sencor SWS 16600 WiFi SH](https://www.sencor.cz/meteorologicka-stanice/sws-16600)
|
- [Sencor SWS 16600 WiFi SH](https://www.sencor.cz/meteorologicka-stanice/sws-16600)
|
||||||
- SWS 10500 (newer firmware revisions are also supported via the [WSLink SSL proxy add-on](https://github.com/schizza/wslink-addon))
|
- Bresser stations that support custom server upload. [for example, this is known to work](https://www.bresser.com/p/bresser-wi-fi-clearview-weather-station-with-7-in-1-sensor-7002586)
|
||||||
- Bresser stations that support custom server upload — [this model is known to work](https://www.bresser.com/p/bresser-wi-fi-clearview-weather-station-with-7-in-1-sensor-7002586), for example
|
- Garni stations with WSLink support or custom server support.
|
||||||
- Garni stations with WSLink support or custom server support
|
|
||||||
- and a bunch of other models that aren't listed here are also supported
|
|
||||||
|
|
||||||
## Supported sensors
|
|
||||||
|
|
||||||
The integration auto-creates sensors as soon as the station first sends data for them — new
|
|
||||||
sensors trigger a notification in Home Assistant and are added to the entity list
|
|
||||||
automatically.
|
|
||||||
|
|
||||||
Beyond the standard set (outdoor / indoor temperature and humidity, barometric pressure,
|
|
||||||
wind speed / direction / gust, rain rate and daily / weekly / monthly / yearly totals, dew
|
|
||||||
point, UV index, solar irradiance) the WSLink protocol also exposes:
|
|
||||||
|
|
||||||
- additional channels **CH2** and **CH3** for temperature and humidity
|
|
||||||
- **WBGT** index, **heat index**, **wind chill**
|
|
||||||
- sensor battery levels (outdoor, indoor, CH2)
|
|
||||||
- **Formaldehyde (HCHO)** in ppb and **VOC level** as a 1–5 air-quality index
|
|
||||||
(1 = worst, 5 = best) from the WH46 / 7-in-1 air-quality combo sensor
|
|
||||||
- **HCHO/VOC sensor battery** reported as a percentage
|
|
||||||
|
|
||||||
HCHO / VOC entities are only created when the station reports the air-quality module as
|
|
||||||
connected (`t9cn = 1`), so they don't clutter the device when no such sensor is attached.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### For stations that send data through WSLink API
|
### If your SWS12500 station's firmware is 1.0 or your station is configured as described in this README and you still can not see any data incoming to Home Assistant please [read here](https://github.com/schizza/SWS-12500-custom-component/issues/17) and [here](firmware_bug.md)
|
||||||
|
|
||||||
Make sure you have your Home Assistant configured in SSL mode or use [WSLink SSL proxy addon](https://github.com/schizza/wslink-addon) to bypass SSL configuration of whole Home Assistant.
|
### For stations that send through WSLink API
|
||||||
|
|
||||||
|
Make sure you have your Home Assistant cofigured in SSL mode or use [WSLink SSL proxy addon](https://github.com/schizza/wslink-addon) to bypass SSL configuration of whole Home Assistant.
|
||||||
|
|
||||||
### HACS installation
|
### HACS installation
|
||||||
|
|
||||||
|
|
@ -137,11 +43,11 @@ After adding this repository to HACS:
|
||||||
|
|
||||||
### Manual installation
|
### Manual installation
|
||||||
|
|
||||||
For manual installation you must have access to your Home Assistant's `/config` folder.
|
For manual installation you must have an access to your Home Assistant's `/config` folder.
|
||||||
|
|
||||||
- Clone this repository or download the [latest release here](https://github.com/schizza/SWS-12500-custom-component/releases/latest).
|
- Clone this repository or download [latest release here](https://github.com/schizza/SWS-12500-custom-component/releases/latest).
|
||||||
|
|
||||||
- Copy the `custom_components/sws12500` folder to your `config/custom_components` folder in Home Assistant.
|
- Copy the `custom_components/sws12500-custom-component` folder to your `config/custom_components` folder in Home Assistant.
|
||||||
- Restart Home Assistant.
|
- Restart Home Assistant.
|
||||||
- Now go to `Integrations` and add new integration `Sencor SWS 12500 Weather station`
|
- Now go to `Integrations` and add new integration `Sencor SWS 12500 Weather station`
|
||||||
|
|
||||||
|
|
@ -177,16 +83,6 @@ If you change `API ID` or `API KEY` in the station, you have to reconfigure inte
|
||||||
|
|
||||||
As soon as the integration is added into Home Assistant it will listen for incoming data from the station and starts to fill sensors as soon as data will first arrive.
|
As soon as the integration is added into Home Assistant it will listen for incoming data from the station and starts to fill sensors as soon as data will first arrive.
|
||||||
|
|
||||||
## Upgrading from PWS to WSLink
|
|
||||||
|
|
||||||
If you upgrade your station — which was previously sending data in the PWS protocol — to a
|
|
||||||
station that uses the WSLink protocol, you have to remove the integration and reinstall it.
|
|
||||||
The WSLink protocol uses the metric scale instead of the imperial scale used in PWS, and
|
|
||||||
deleting and reinstalling the integration makes sure the sensors are aware of the change of
|
|
||||||
measurement scale.
|
|
||||||
|
|
||||||
- because sensor unique IDs stay the same, you will not lose any of your historical data
|
|
||||||
|
|
||||||
## Resending data to Windy API
|
## Resending data to Windy API
|
||||||
|
|
||||||
- First of all you need to create account at [Windy stations](https://stations.windy.com).
|
- First of all you need to create account at [Windy stations](https://stations.windy.com).
|
||||||
|
|
@ -203,39 +99,22 @@ measurement scale.
|
||||||
|
|
||||||
- You are done.
|
- You are done.
|
||||||
|
|
||||||
## Resending data to Pocasi Meteo
|
|
||||||
|
|
||||||
- If you are willing to use [Pocasi Meteo Application](https://pocasimeteo.cz) you can enable resending your data to their servers
|
|
||||||
- You must have account at Pocasi Meteo, where you will recieve `ID` and `KEY`, which are needed to connect to server
|
|
||||||
- In `Settings` -> `Devices & services` find SWS12500 and click `Configure`.
|
|
||||||
- In dialog box choose `Pocasi Meteo configuration`.
|
|
||||||
- Fill in `ID` and `KEY` you were provided at `Pocasi Meteo`.
|
|
||||||
- Tick `Enable` checkbox.
|
|
||||||
|
|
||||||
- You are done.
|
|
||||||
|
|
||||||
## WSLink notes
|
## WSLink notes
|
||||||
|
|
||||||
If your station sends WSLink data over SSL (see the
|
While your station is using WSLink you have to have Home Assistant in SSL mode or behind SSL proxy server.
|
||||||
[Quick Recommendations](#quick-recommendations) table above), Home Assistant has to be
|
You can bypass whole SSL settings by using [WSLink SSL proxy addon](https://github.com/schizza/wslink-addon) which is made exactly for this integration to support WSLink on unsecured installations of Home Assistant.
|
||||||
reachable over SSL too — either by running Home Assistant in SSL mode, or by putting it
|
|
||||||
behind an SSL proxy. You can bypass whole-system SSL setup by using the
|
|
||||||
[WSLink SSL proxy add-on](https://github.com/schizza/wslink-addon), which is made exactly
|
|
||||||
for this integration to support WSLink on non-SSL installations of Home Assistant.
|
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
- Set your station up as [described above](#configure-your-station-in-ap-mode), but for
|
- Set your station as [mentioned above](#configure-your-station-in-ap-mode) while changing `HA port` to be the port number you set in the addon (443 for example) not port of your Home Assistant instance. And that will do the trick!
|
||||||
`HA port` use the port the add-on is listening on (4443 by default) — **not** the port
|
|
||||||
of your Home Assistant instance.
|
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
Home Assistant is at 192.168.0.2:8123
|
HomeAssistant is at 192.0.0.2:8123
|
||||||
WSLink proxy add-on is listening on port 4443
|
WSLink proxy addon listening on port 4443
|
||||||
|
|
||||||
→ set the station URL to: 192.168.0.2:4443
|
you will set URL in station to: 192.0.0.2:4443
|
||||||
```
|
```
|
||||||
|
|
||||||
- Your station will send data to the SSL proxy and the add-on will handle the rest.
|
- Your station will be sending data to this SSL proxy and addon will handle the rest.
|
||||||
|
|
||||||
_Most stations do not care about self-signed certificates on the server side._
|
_Most of the stations does not care about self-signed certificates on the server side._
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import aiohttp.web
|
import aiohttp
|
||||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
|
@ -17,13 +17,11 @@ from .const import (
|
||||||
DEFAULT_URL,
|
DEFAULT_URL,
|
||||||
DEV_DBG,
|
DEV_DBG,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
POCASI_CZ_ENABLED,
|
|
||||||
SENSORS_TO_LOAD,
|
SENSORS_TO_LOAD,
|
||||||
WINDY_ENABLED,
|
WINDY_ENABLED,
|
||||||
WSLINK,
|
WSLINK,
|
||||||
WSLINK_URL,
|
WSLINK_URL,
|
||||||
)
|
)
|
||||||
from .pocasti_cz import PocasiPush
|
|
||||||
from .routes import Routes, unregistred
|
from .routes import Routes, unregistred
|
||||||
from .utils import (
|
from .utils import (
|
||||||
anonymize,
|
anonymize,
|
||||||
|
|
@ -52,18 +50,13 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Init global updater."""
|
"""Init global updater."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.config = config
|
self.config = config
|
||||||
self.config_entry = config
|
|
||||||
self.windy = WindyPush(hass, config)
|
self.windy = WindyPush(hass, config)
|
||||||
self.pocasi: PocasiPush = PocasiPush(hass, config)
|
|
||||||
super().__init__(hass, _LOGGER, name=DOMAIN)
|
super().__init__(hass, _LOGGER, name=DOMAIN)
|
||||||
|
|
||||||
async def recieved_data(self, webdata: aiohttp.web.Request):
|
async def recieved_data(self, webdata):
|
||||||
"""Handle incoming data query."""
|
"""Handle incoming data query."""
|
||||||
_wslink = self.config_entry.options.get(WSLINK)
|
_wslink = self.config_entry.options.get(WSLINK)
|
||||||
get_data = webdata.query
|
data = webdata.query
|
||||||
post_data = await webdata.post()
|
|
||||||
|
|
||||||
data = dict(get_data) | dict(post_data)
|
|
||||||
|
|
||||||
response = None
|
response = None
|
||||||
|
|
||||||
|
|
@ -90,10 +83,7 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
raise HTTPUnauthorized
|
raise HTTPUnauthorized
|
||||||
|
|
||||||
if self.config_entry.options.get(WINDY_ENABLED):
|
if self.config_entry.options.get(WINDY_ENABLED):
|
||||||
_ = await self.windy.push_data_to_windy(data, _wslink)
|
response = await self.windy.push_data_to_windy(data)
|
||||||
|
|
||||||
if self.config.options.get(POCASI_CZ_ENABLED):
|
|
||||||
await self.pocasi.push_data_to_server(data, "WSLINK" if _wslink else "WU")
|
|
||||||
|
|
||||||
remaped_items = (
|
remaped_items = (
|
||||||
remap_wslink_items(data)
|
remap_wslink_items(data)
|
||||||
|
|
@ -144,11 +134,11 @@ def register_path(
|
||||||
|
|
||||||
hass_data = hass.data.setdefault(DOMAIN, {})
|
hass_data = hass.data.setdefault(DOMAIN, {})
|
||||||
debug = config.options.get(DEV_DBG)
|
debug = config.options.get(DEV_DBG)
|
||||||
_wslink = config.options.get(WSLINK, False)
|
_wslink = config.options.get(WSLINK)
|
||||||
|
|
||||||
routes: Routes = hass_data.get("routes", Routes())
|
routes: Routes = hass_data.get("routes") if "routes" in hass_data else None
|
||||||
|
|
||||||
if not routes.routes:
|
if routes is None:
|
||||||
routes = Routes()
|
routes = Routes()
|
||||||
_LOGGER.info("Routes not found, creating new routes")
|
_LOGGER.info("Routes not found, creating new routes")
|
||||||
|
|
||||||
|
|
@ -172,14 +162,6 @@ def register_path(
|
||||||
if debug:
|
if debug:
|
||||||
_LOGGER.debug("WSLink route: %s", wslink_route)
|
_LOGGER.debug("WSLink route: %s", wslink_route)
|
||||||
|
|
||||||
wslink_post_route = hass.http.app.router.add_post(
|
|
||||||
WSLINK_URL,
|
|
||||||
coordinator.recieved_data if _wslink else unregistred,
|
|
||||||
name="weather_wslink_post_route_url",
|
|
||||||
)
|
|
||||||
if debug:
|
|
||||||
_LOGGER.debug("WSLink route: %s", wslink_post_route)
|
|
||||||
|
|
||||||
routes.add_route(
|
routes.add_route(
|
||||||
DEFAULT_URL,
|
DEFAULT_URL,
|
||||||
default_route,
|
default_route,
|
||||||
|
|
@ -190,10 +172,6 @@ def register_path(
|
||||||
WSLINK_URL, wslink_route, coordinator.recieved_data, _wslink
|
WSLINK_URL, wslink_route, coordinator.recieved_data, _wslink
|
||||||
)
|
)
|
||||||
|
|
||||||
routes.add_route(
|
|
||||||
WSLINK_URL, wslink_post_route, coordinator.recieved_data, _wslink
|
|
||||||
)
|
|
||||||
|
|
||||||
hass_data["routes"] = routes
|
hass_data["routes"] = routes
|
||||||
|
|
||||||
except RuntimeError as Ex: # pylint: disable=(broad-except)
|
except RuntimeError as Ex: # pylint: disable=(broad-except)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
"""Config flow for Sencor SWS 12500 Weather Station integration."""
|
"""Config flow for Sencor SWS 12500 Weather Station integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow
|
from homeassistant.config_entries import ConfigFlow, OptionsFlow
|
||||||
|
from homeassistant.const import UnitOfPrecipitationDepth, UnitOfVolumetricFlux
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
API_ID,
|
API_ID,
|
||||||
|
|
@ -14,19 +17,18 @@ from .const import (
|
||||||
DEV_DBG,
|
DEV_DBG,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
INVALID_CREDENTIALS,
|
INVALID_CREDENTIALS,
|
||||||
POCASI_CZ_API_ID,
|
MIG_FROM,
|
||||||
POCASI_CZ_API_KEY,
|
MIG_TO,
|
||||||
POCASI_CZ_ENABLED,
|
SENSOR_TO_MIGRATE,
|
||||||
POCASI_CZ_LOGGER_ENABLED,
|
|
||||||
POCASI_CZ_SEND_INTERVAL,
|
|
||||||
POCASI_CZ_SEND_MINIMUM,
|
|
||||||
SENSORS_TO_LOAD,
|
SENSORS_TO_LOAD,
|
||||||
|
WINDY_API_KEY,
|
||||||
WINDY_ENABLED,
|
WINDY_ENABLED,
|
||||||
WINDY_LOGGER_ENABLED,
|
WINDY_LOGGER_ENABLED,
|
||||||
WINDY_STATION_ID,
|
|
||||||
WINDY_STATION_PW,
|
|
||||||
WSLINK,
|
WSLINK,
|
||||||
)
|
)
|
||||||
|
from .utils import long_term_units_in_statistics_meta, migrate_data
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CannotConnect(HomeAssistantError):
|
class CannotConnect(HomeAssistantError):
|
||||||
|
|
@ -43,7 +45,6 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize flow."""
|
"""Initialize flow."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# self.config_entry = config_entry
|
|
||||||
|
|
||||||
self.windy_data: dict[str, Any] = {}
|
self.windy_data: dict[str, Any] = {}
|
||||||
self.windy_data_schema = {}
|
self.windy_data_schema = {}
|
||||||
|
|
@ -51,18 +52,26 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
self.user_data_schema = {}
|
self.user_data_schema = {}
|
||||||
self.sensors: dict[str, Any] = {}
|
self.sensors: dict[str, Any] = {}
|
||||||
self.migrate_schema = {}
|
self.migrate_schema = {}
|
||||||
self.pocasi_cz: dict[str, Any] = {}
|
self.migrate_sensor_select = {}
|
||||||
self.pocasi_cz_schema = {}
|
self.migrate_unit_selection = {}
|
||||||
|
self.count = 0
|
||||||
|
self.selected_sensor = ""
|
||||||
|
|
||||||
|
self.unit_values = [unit.value for unit in UnitOfVolumetricFlux]
|
||||||
|
self.unit_values.extend([unit.value for unit in UnitOfPrecipitationDepth])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config_entry(self):
|
||||||
|
return self.hass.config_entries.async_get_entry(self.handler)
|
||||||
|
|
||||||
async def _get_entry_data(self):
|
async def _get_entry_data(self):
|
||||||
"""Get entry data."""
|
"""Get entry data."""
|
||||||
entry_data = {**self.config_entry.data, **self.config_entry.options}
|
|
||||||
|
|
||||||
self.user_data = {
|
self.user_data: dict[str, Any] = {
|
||||||
API_ID: entry_data.get(API_ID),
|
API_ID: self.config_entry.options.get(API_ID),
|
||||||
API_KEY: entry_data.get(API_KEY),
|
API_KEY: self.config_entry.options.get(API_KEY),
|
||||||
WSLINK: entry_data.get(WSLINK, False),
|
WSLINK: self.config_entry.options.get(WSLINK, False),
|
||||||
DEV_DBG: entry_data.get(DEV_DBG, False),
|
DEV_DBG: self.config_entry.options.get(DEV_DBG, False),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.user_data_schema = {
|
self.user_data_schema = {
|
||||||
|
|
@ -72,54 +81,66 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
vol.Optional(DEV_DBG, default=self.user_data.get(DEV_DBG, False)): bool,
|
vol.Optional(DEV_DBG, default=self.user_data.get(DEV_DBG, False)): bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sensors = {
|
self.sensors: dict[str, Any] = {
|
||||||
SENSORS_TO_LOAD: (
|
SENSORS_TO_LOAD: self.config_entry.options.get(SENSORS_TO_LOAD)
|
||||||
entry_data.get(SENSORS_TO_LOAD) if isinstance(entry_data.get(SENSORS_TO_LOAD), list) else []
|
if isinstance(self.config_entry.options.get(SENSORS_TO_LOAD), list)
|
||||||
)
|
else []
|
||||||
}
|
}
|
||||||
|
|
||||||
self.windy_data = {
|
self.windy_data: dict[str, Any] = {
|
||||||
WINDY_STATION_ID: entry_data.get(WINDY_STATION_ID),
|
WINDY_API_KEY: self.config_entry.options.get(WINDY_API_KEY),
|
||||||
WINDY_STATION_PW: entry_data.get(WINDY_STATION_PW),
|
WINDY_ENABLED: self.config_entry.options.get(WINDY_ENABLED, False),
|
||||||
WINDY_ENABLED: entry_data.get(WINDY_ENABLED, False),
|
WINDY_LOGGER_ENABLED: self.config_entry.options.get(
|
||||||
WINDY_LOGGER_ENABLED: entry_data.get(WINDY_LOGGER_ENABLED, False),
|
WINDY_LOGGER_ENABLED, False
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.windy_data_schema = {
|
self.windy_data_schema = {
|
||||||
vol.Optional(WINDY_STATION_ID, default=self.windy_data.get(WINDY_STATION_ID, "")): str,
|
vol.Optional(
|
||||||
vol.Optional(WINDY_STATION_PW, default=self.windy_data.get(WINDY_STATION_PW, "")): str,
|
WINDY_API_KEY, default=self.windy_data.get(WINDY_API_KEY, "")
|
||||||
vol.Optional(WINDY_ENABLED, default=self.windy_data[WINDY_ENABLED]): bool or False,
|
): str,
|
||||||
|
vol.Optional(WINDY_ENABLED, default=self.windy_data[WINDY_ENABLED]): bool
|
||||||
|
or False,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
WINDY_LOGGER_ENABLED,
|
WINDY_LOGGER_ENABLED,
|
||||||
default=self.windy_data[WINDY_LOGGER_ENABLED],
|
default=self.windy_data[WINDY_LOGGER_ENABLED],
|
||||||
): bool or False,
|
): bool or False,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pocasi_cz = {
|
self.migrate_sensor_select = {
|
||||||
POCASI_CZ_API_ID: entry_data.get(POCASI_CZ_API_ID, ""),
|
vol.Required(SENSOR_TO_MIGRATE): vol.In(
|
||||||
POCASI_CZ_API_KEY: entry_data.get(POCASI_CZ_API_KEY, ""),
|
await self.load_sensors_to_migrate() or {}
|
||||||
POCASI_CZ_ENABLED: entry_data.get(POCASI_CZ_ENABLED, False),
|
),
|
||||||
POCASI_CZ_LOGGER_ENABLED: entry_data.get(POCASI_CZ_LOGGER_ENABLED, False),
|
|
||||||
POCASI_CZ_SEND_INTERVAL: entry_data.get(POCASI_CZ_SEND_INTERVAL, 30),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pocasi_cz_schema = {
|
self.migrate_unit_selection = {
|
||||||
vol.Required(POCASI_CZ_API_ID, default=self.pocasi_cz.get(POCASI_CZ_API_ID)): str,
|
vol.Required(MIG_FROM): vol.In(self.unit_values),
|
||||||
vol.Required(POCASI_CZ_API_KEY, default=self.pocasi_cz.get(POCASI_CZ_API_KEY)): str,
|
vol.Required(MIG_TO): vol.In(self.unit_values),
|
||||||
vol.Required(
|
vol.Optional("trigger_action", default=False): bool,
|
||||||
POCASI_CZ_SEND_INTERVAL,
|
}
|
||||||
default=self.pocasi_cz.get(POCASI_CZ_SEND_INTERVAL),
|
# "mm/d", "mm/h", "mm", "in/d", "in/h", "in"
|
||||||
): int,
|
|
||||||
vol.Optional(POCASI_CZ_ENABLED, default=self.pocasi_cz.get(POCASI_CZ_ENABLED)): bool,
|
async def load_sensors_to_migrate(self) -> dict[str, Any]:
|
||||||
vol.Optional(
|
"""Load sensors to migrate."""
|
||||||
POCASI_CZ_LOGGER_ENABLED,
|
|
||||||
default=self.pocasi_cz.get(POCASI_CZ_LOGGER_ENABLED),
|
sensor_statistics = await long_term_units_in_statistics_meta(self.hass)
|
||||||
): bool,
|
|
||||||
|
entity_registry = er.async_get(self.hass)
|
||||||
|
sensors = entity_registry.entities.get_entries_for_config_entry_id(
|
||||||
|
self.config_entry.entry_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
sensor.entity_id: f"{sensor.name or sensor.original_name} (current settings: {sensor.unit_of_measurement}, longterm stats unit: {sensor_statistics.get(sensor.entity_id)})"
|
||||||
|
for sensor in sensors
|
||||||
|
if sensor.unique_id in {"rain", "daily_rain"}
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_step_init(self, user_input=None):
|
async def async_step_init(self, user_input=None):
|
||||||
"""Manage the options - show menu first."""
|
"""Manage the options - show menu first."""
|
||||||
return self.async_show_menu(step_id="init", menu_options=["basic", "windy", "pocasi"])
|
return self.async_show_menu(
|
||||||
|
step_id="init", menu_options=["basic", "windy", "migration"]
|
||||||
|
)
|
||||||
|
|
||||||
async def async_step_basic(self, user_input=None):
|
async def async_step_basic(self, user_input=None):
|
||||||
"""Manage basic options - credentials."""
|
"""Manage basic options - credentials."""
|
||||||
|
|
@ -147,9 +168,6 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
# retain sensors
|
# retain sensors
|
||||||
user_input.update(self.sensors)
|
user_input.update(self.sensors)
|
||||||
|
|
||||||
# retain pocasi data
|
|
||||||
user_input.update(self.pocasi_cz)
|
|
||||||
|
|
||||||
return self.async_create_entry(title=DOMAIN, data=user_input)
|
return self.async_create_entry(title=DOMAIN, data=user_input)
|
||||||
|
|
||||||
self.user_data = user_input
|
self.user_data = user_input
|
||||||
|
|
@ -174,14 +192,8 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
station_id = (user_input.get(WINDY_STATION_ID) or "").strip()
|
if (user_input[WINDY_ENABLED] is True) and (user_input[WINDY_API_KEY] == ""):
|
||||||
station_pw = (user_input.get(WINDY_STATION_PW) or "").strip()
|
errors[WINDY_API_KEY] = "windy_key_required"
|
||||||
if user_input.get(WINDY_ENABLED):
|
|
||||||
if not station_id:
|
|
||||||
errors[WINDY_STATION_ID] = "windy_id_required"
|
|
||||||
if not station_pw:
|
|
||||||
errors[WINDY_STATION_PW] = "windy_pw_required"
|
|
||||||
if errors:
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="windy",
|
step_id="windy",
|
||||||
data_schema=vol.Schema(self.windy_data_schema),
|
data_schema=vol.Schema(self.windy_data_schema),
|
||||||
|
|
@ -194,54 +206,155 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
# retain senors
|
# retain senors
|
||||||
user_input.update(self.sensors)
|
user_input.update(self.sensors)
|
||||||
|
|
||||||
# retain pocasi cz
|
|
||||||
|
|
||||||
user_input.update(self.pocasi_cz)
|
|
||||||
|
|
||||||
return self.async_create_entry(title=DOMAIN, data=user_input)
|
return self.async_create_entry(title=DOMAIN, data=user_input)
|
||||||
|
|
||||||
async def async_step_pocasi(self, user_input: Any = None) -> ConfigFlowResult:
|
async def async_step_migration(self, user_input=None):
|
||||||
"""Handle the pocasi step."""
|
"""Migrate sensors."""
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
|
data_schema = vol.Schema(self.migrate_sensor_select)
|
||||||
|
data_schema.schema.update()
|
||||||
|
|
||||||
await self._get_entry_data()
|
await self._get_entry_data()
|
||||||
|
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="pocasi",
|
step_id="migration",
|
||||||
data_schema=vol.Schema(self.pocasi_cz_schema),
|
data_schema=vol.Schema(self.migrate_sensor_select),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"migration_status": "-",
|
||||||
|
"migration_count": "-",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if user_input.get(POCASI_CZ_SEND_INTERVAL, 0) < POCASI_CZ_SEND_MINIMUM:
|
self.selected_sensor = user_input.get(SENSOR_TO_MIGRATE)
|
||||||
errors[POCASI_CZ_SEND_INTERVAL] = "pocasi_send_minimum"
|
|
||||||
|
|
||||||
if user_input.get(POCASI_CZ_ENABLED):
|
return await self.async_step_migration_units()
|
||||||
if user_input.get(POCASI_CZ_API_ID) == "":
|
|
||||||
errors[POCASI_CZ_API_ID] = "pocasi_id_required"
|
|
||||||
if user_input.get(POCASI_CZ_API_KEY) == "":
|
|
||||||
errors[POCASI_CZ_API_KEY] = "pocasi_key_required"
|
|
||||||
|
|
||||||
if len(errors) > 0:
|
async def async_step_migration_units(self, user_input=None):
|
||||||
|
"""Migrate unit step."""
|
||||||
|
|
||||||
|
registry = er.async_get(self.hass)
|
||||||
|
sensor_entry = registry.async_get(self.selected_sensor)
|
||||||
|
sensor_stats = await long_term_units_in_statistics_meta(self.hass)
|
||||||
|
|
||||||
|
default_unit = sensor_entry.unit_of_measurement if sensor_entry else None
|
||||||
|
|
||||||
|
if default_unit not in self.unit_values:
|
||||||
|
default_unit = self.unit_values[0]
|
||||||
|
|
||||||
|
data_schema = vol.Schema({
|
||||||
|
vol.Required(MIG_FROM, default=default_unit): vol.In(self.unit_values),
|
||||||
|
vol.Required(MIG_TO): vol.In(self.unit_values),
|
||||||
|
vol.Optional("trigger_action", default=False): bool,
|
||||||
|
})
|
||||||
|
|
||||||
|
if user_input is None:
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="pocasi",
|
step_id="migration_units",
|
||||||
data_schema=vol.Schema(self.pocasi_cz_schema),
|
data_schema=data_schema,
|
||||||
errors=errors,
|
errors={},
|
||||||
|
description_placeholders={
|
||||||
|
"migration_sensor": sensor_entry.original_name,
|
||||||
|
"migration_stats": sensor_stats.get(self.selected_sensor),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
# retain user data
|
|
||||||
|
if user_input.get("trigger_action"):
|
||||||
|
self.count = await migrate_data(
|
||||||
|
self.hass,
|
||||||
|
self.selected_sensor,
|
||||||
|
user_input.get(MIG_FROM),
|
||||||
|
user_input.get(MIG_TO),
|
||||||
|
)
|
||||||
|
|
||||||
|
registry.async_update_entity(self.selected_sensor,
|
||||||
|
unit_of_measurement=user_input.get(MIG_TO),
|
||||||
|
)
|
||||||
|
|
||||||
|
state = self.hass.states.get(self.selected_sensor)
|
||||||
|
if state:
|
||||||
|
_LOGGER.info("State attributes before update: %s", state.attributes)
|
||||||
|
attributes = dict(state.attributes)
|
||||||
|
attributes["unit_of_measurement"] = user_input.get(MIG_TO)
|
||||||
|
self.hass.states.async_set(self.selected_sensor, state.state, attributes)
|
||||||
|
_LOGGER.info("State attributes after update: %s", attributes)
|
||||||
|
|
||||||
|
options = {**self.config_entry.options, "reload_sensor": self.selected_sensor}
|
||||||
|
self.hass.config_entries.async_update_entry(self.config_entry, options=options)
|
||||||
|
|
||||||
|
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||||
|
|
||||||
|
await self.hass.async_block_till_done()
|
||||||
|
|
||||||
|
_LOGGER.info("Migration complete for sensor %s: %s row updated, new measurement unit: %s, ",
|
||||||
|
self.selected_sensor,
|
||||||
|
self.count,
|
||||||
|
user_input.get(MIG_TO),
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._get_entry_data()
|
||||||
|
sensor_entry = er.async_get(self.hass).async_get(self.selected_sensor)
|
||||||
|
sensor_stat = await self.load_sensors_to_migrate()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="migration_complete",
|
||||||
|
data_schema=vol.Schema({}),
|
||||||
|
errors={},
|
||||||
|
description_placeholders={
|
||||||
|
"migration_sensor": sensor_entry.unit_of_measurement,
|
||||||
|
"migration_stats": sensor_stat.get(self.selected_sensor),
|
||||||
|
"migration_count": self.count,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# retain windy data
|
||||||
|
user_input.update(self.windy_data)
|
||||||
|
|
||||||
|
# retain user_data
|
||||||
user_input.update(self.user_data)
|
user_input.update(self.user_data)
|
||||||
|
|
||||||
# retain senors
|
# retain senors
|
||||||
user_input.update(self.sensors)
|
user_input.update(self.sensors)
|
||||||
|
|
||||||
# retain windy
|
return self.async_create_entry(title=DOMAIN, data=user_input)
|
||||||
|
|
||||||
|
async def async_step_migration_complete(self, user_input=None):
|
||||||
|
"""Migration complete."""
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
await self._get_entry_data()
|
||||||
|
sensor_entry = er.async_get(self.hass).async_get(self.selected_sensor)
|
||||||
|
sensor_stat = await self.load_sensors_to_migrate()
|
||||||
|
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="migration_complete",
|
||||||
|
data_schema=vol.Schema({}),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"migration_sensor": sensor_entry.unit_of_measurement,
|
||||||
|
"migration_stats": sensor_stat.get(self.selected_sensor),
|
||||||
|
"migration_count": self.count,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# retain windy data
|
||||||
user_input.update(self.windy_data)
|
user_input.update(self.windy_data)
|
||||||
|
|
||||||
|
# retain user_data
|
||||||
|
user_input.update(self.user_data)
|
||||||
|
|
||||||
|
# retain senors
|
||||||
|
user_input.update(self.sensors)
|
||||||
|
|
||||||
return self.async_create_entry(title=DOMAIN, data=user_input)
|
return self.async_create_entry(title=DOMAIN, data=user_input)
|
||||||
|
|
||||||
|
|
||||||
class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
class ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Sencor SWS 12500 Weather Station."""
|
"""Handle a config flow for Sencor SWS 12500 Weather Station."""
|
||||||
|
|
||||||
data_schema = {
|
data_schema = {
|
||||||
|
|
@ -273,7 +386,9 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
elif user_input[API_KEY] == user_input[API_ID]:
|
elif user_input[API_KEY] == user_input[API_ID]:
|
||||||
errors["base"] = "valid_credentials_match"
|
errors["base"] = "valid_credentials_match"
|
||||||
else:
|
else:
|
||||||
return self.async_create_entry(title=DOMAIN, data=user_input, options=user_input)
|
return self.async_create_entry(
|
||||||
|
title=DOMAIN, data=user_input, options=user_input
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
|
|
@ -283,6 +398,6 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(config_entry: ConfigEntry) -> ConfigOptionsFlowHandler:
|
def async_get_options_flow(config_entry) -> ConfigOptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return ConfigOptionsFlowHandler()
|
return ConfigOptionsFlowHandler()
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,9 @@ from typing import Final
|
||||||
DOMAIN = "sws12500"
|
DOMAIN = "sws12500"
|
||||||
DEFAULT_URL = "/weatherstation/updateweatherstation.php"
|
DEFAULT_URL = "/weatherstation/updateweatherstation.php"
|
||||||
WSLINK_URL = "/data/upload.php"
|
WSLINK_URL = "/data/upload.php"
|
||||||
WINDY_URL = "https://stations.windy.com/api/v2/observation/update"
|
WINDY_URL = "https://stations.windy.com/pws/update/"
|
||||||
DATABASE_PATH = "/config/home-assistant_v2.db"
|
DATABASE_PATH = "/config/home-assistant_v2.db"
|
||||||
|
|
||||||
POCASI_CZ_URL: Final = "http://ms.pocasimeteo.cz"
|
|
||||||
POCASI_CZ_SEND_MINIMUM: Final = 12 # minimal time to resend data
|
|
||||||
|
|
||||||
ICON = "mdi:weather"
|
ICON = "mdi:weather"
|
||||||
|
|
||||||
API_KEY = "API_KEY"
|
API_KEY = "API_KEY"
|
||||||
|
|
@ -23,27 +20,17 @@ SENSOR_TO_MIGRATE: Final = "sensor_to_migrate"
|
||||||
DEV_DBG: Final = "dev_debug_checkbox"
|
DEV_DBG: Final = "dev_debug_checkbox"
|
||||||
WSLINK: Final = "wslink"
|
WSLINK: Final = "wslink"
|
||||||
|
|
||||||
POCASI_CZ_API_KEY = "POCASI_CZ_API_KEY"
|
WINDY_API_KEY = "WINDY_API_KEY"
|
||||||
POCASI_CZ_API_ID = "POCASI_CZ_API_ID"
|
|
||||||
POCASI_CZ_SEND_INTERVAL = "POCASI_SEND_INTERVAL"
|
|
||||||
POCASI_CZ_ENABLED = "pocasi_enabled_chcekbox"
|
|
||||||
POCASI_CZ_LOGGER_ENABLED = "pocasi_logger_checkbox"
|
|
||||||
POCASI_INVALID_KEY: Final = "Pocasi Meteo refused to accept data. Invalid ID/Key combination?"
|
|
||||||
POCASI_CZ_SUCCESS: Final = "Successfully sent data to Pocasi Meteo"
|
|
||||||
POCASI_CZ_UNEXPECTED: Final = "Pocasti Meteo responded unexpectedly 3 times in row. Resendig is now disabled!"
|
|
||||||
|
|
||||||
WINDY_STATION_ID = "WINDY_STATION_ID"
|
|
||||||
WINDY_STATION_PW = "WINDY_STATION_PWD"
|
|
||||||
WINDY_ENABLED: Final = "windy_enabled_checkbox"
|
WINDY_ENABLED: Final = "windy_enabled_checkbox"
|
||||||
WINDY_LOGGER_ENABLED: Final = "windy_logger_checkbox"
|
WINDY_LOGGER_ENABLED: Final = "windy_logger_checkbox"
|
||||||
WINDY_NOT_INSERTED: Final = (
|
WINDY_NOT_INSERTED: Final = "Data was succefuly sent to Windy, but not inserted by Windy API. Does anyone else sent data to Windy?"
|
||||||
"Data was succefuly sent to Windy, but not inserted by Windy API. Does anyone else sent data to Windy?"
|
WINDY_INVALID_KEY: Final = "Windy API KEY is invalid. Send data to Windy is now disabled. Check your API KEY and try again."
|
||||||
|
WINDY_SUCCESS: Final = (
|
||||||
|
"Windy successfully sent data and data was successfully inserted by Windy API"
|
||||||
)
|
)
|
||||||
WINDY_INVALID_KEY: Final = (
|
WINDY_UNEXPECTED: Final = (
|
||||||
"Windy API KEY is invalid. Send data to Windy is now disabled. Check your API KEY and try again."
|
"Windy responded unexpectedly 3 times in a row. Send to Windy is now disabled!"
|
||||||
)
|
)
|
||||||
WINDY_SUCCESS: Final = "Windy successfully sent data and data was successfully inserted by Windy API"
|
|
||||||
WINDY_UNEXPECTED: Final = "Windy responded unexpectedly 3 times in a row. Send to Windy is now disabled!"
|
|
||||||
|
|
||||||
INVALID_CREDENTIALS: Final = [
|
INVALID_CREDENTIALS: Final = [
|
||||||
"API",
|
"API",
|
||||||
|
|
@ -68,15 +55,6 @@ PURGE_DATA: Final = [
|
||||||
"indoortempf",
|
"indoortempf",
|
||||||
"indoorhumidity",
|
"indoorhumidity",
|
||||||
"dailyrainin",
|
"dailyrainin",
|
||||||
"wspw",
|
|
||||||
"wsid",
|
|
||||||
]
|
|
||||||
|
|
||||||
PURGE_DATA_POCAS: Final = [
|
|
||||||
"ID",
|
|
||||||
"PASSWORD",
|
|
||||||
"action",
|
|
||||||
"rtfreq",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -85,7 +63,6 @@ OUTSIDE_TEMP: Final = "outside_temp"
|
||||||
DEW_POINT: Final = "dew_point"
|
DEW_POINT: Final = "dew_point"
|
||||||
OUTSIDE_HUMIDITY: Final = "outside_humidity"
|
OUTSIDE_HUMIDITY: Final = "outside_humidity"
|
||||||
OUTSIDE_CONNECTION: Final = "outside_connection"
|
OUTSIDE_CONNECTION: Final = "outside_connection"
|
||||||
OUTSIDE_BATTERY: Final = "outside_battery"
|
|
||||||
WIND_SPEED: Final = "wind_speed"
|
WIND_SPEED: Final = "wind_speed"
|
||||||
WIND_GUST: Final = "wind_gust"
|
WIND_GUST: Final = "wind_gust"
|
||||||
WIND_DIR: Final = "wind_dir"
|
WIND_DIR: Final = "wind_dir"
|
||||||
|
|
@ -99,12 +76,10 @@ DAILY_RAIN: Final = "daily_rain"
|
||||||
SOLAR_RADIATION: Final = "solar_radiation"
|
SOLAR_RADIATION: Final = "solar_radiation"
|
||||||
INDOOR_TEMP: Final = "indoor_temp"
|
INDOOR_TEMP: Final = "indoor_temp"
|
||||||
INDOOR_HUMIDITY: Final = "indoor_humidity"
|
INDOOR_HUMIDITY: Final = "indoor_humidity"
|
||||||
INDOOR_BATTERY: Final = "indoor_battery"
|
|
||||||
UV: Final = "uv"
|
UV: Final = "uv"
|
||||||
CH2_TEMP: Final = "ch2_temp"
|
CH2_TEMP: Final = "ch2_temp"
|
||||||
CH2_HUMIDITY: Final = "ch2_humidity"
|
CH2_HUMIDITY: Final = "ch2_humidity"
|
||||||
CH2_CONNECTION: Final = "ch2_connection"
|
CH2_CONNECTION: Final = "ch2_connection"
|
||||||
CH2_BATTERY: Final = "ch2_battery"
|
|
||||||
CH3_TEMP: Final = "ch3_temp"
|
CH3_TEMP: Final = "ch3_temp"
|
||||||
CH3_HUMIDITY: Final = "ch3_humidity"
|
CH3_HUMIDITY: Final = "ch3_humidity"
|
||||||
CH3_CONNECTION: Final = "ch3_connection"
|
CH3_CONNECTION: Final = "ch3_connection"
|
||||||
|
|
@ -113,14 +88,9 @@ CH4_HUMIDITY: Final = "ch4_humidity"
|
||||||
CH4_CONNECTION: Final = "ch4_connection"
|
CH4_CONNECTION: Final = "ch4_connection"
|
||||||
HEAT_INDEX: Final = "heat_index"
|
HEAT_INDEX: Final = "heat_index"
|
||||||
CHILL_INDEX: Final = "chill_index"
|
CHILL_INDEX: Final = "chill_index"
|
||||||
WBGT_TEMP: Final = "wbgt_temp"
|
|
||||||
HCHO: Final = "hcho"
|
|
||||||
VOC: Final = "voc"
|
|
||||||
T9_BATTERY: Final = "t9_battery" # T9 sensors are HCHO and VOC
|
|
||||||
T9_CONN: Final = "t9_conn"
|
|
||||||
|
|
||||||
|
|
||||||
REMAP_ITEMS: dict[str, str] = {
|
REMAP_ITEMS: dict = {
|
||||||
"baromin": BARO_PRESSURE,
|
"baromin": BARO_PRESSURE,
|
||||||
"tempf": OUTSIDE_TEMP,
|
"tempf": OUTSIDE_TEMP,
|
||||||
"dewptf": DEW_POINT,
|
"dewptf": DEW_POINT,
|
||||||
|
|
@ -142,7 +112,7 @@ REMAP_ITEMS: dict[str, str] = {
|
||||||
"soilmoisture3": CH4_HUMIDITY,
|
"soilmoisture3": CH4_HUMIDITY,
|
||||||
}
|
}
|
||||||
|
|
||||||
REMAP_WSLINK_ITEMS: dict[str, str] = {
|
REMAP_WSLINK_ITEMS: dict = {
|
||||||
"intem": INDOOR_TEMP,
|
"intem": INDOOR_TEMP,
|
||||||
"inhum": INDOOR_HUMIDITY,
|
"inhum": INDOOR_HUMIDITY,
|
||||||
"t1tem": OUTSIDE_TEMP,
|
"t1tem": OUTSIDE_TEMP,
|
||||||
|
|
@ -167,60 +137,24 @@ REMAP_WSLINK_ITEMS: dict[str, str] = {
|
||||||
"t1rainwy": WEEKLY_RAIN,
|
"t1rainwy": WEEKLY_RAIN,
|
||||||
"t1rainmth": MONTHLY_RAIN,
|
"t1rainmth": MONTHLY_RAIN,
|
||||||
"t1rainyr": YEARLY_RAIN,
|
"t1rainyr": YEARLY_RAIN,
|
||||||
"t234c2tem": CH3_TEMP,
|
|
||||||
"t234c2hum": CH3_HUMIDITY,
|
|
||||||
"t1bat": OUTSIDE_BATTERY,
|
|
||||||
"inbat": INDOOR_BATTERY,
|
|
||||||
"t234c1bat": CH2_BATTERY,
|
|
||||||
"t1wbgt": WBGT_TEMP,
|
|
||||||
"t9hcho": HCHO,
|
|
||||||
"t9voclv": VOC,
|
|
||||||
"t9bat": T9_BATTERY, # T9 battery is 0-5, where 5 is full
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# TODO: Add more sensors
|
||||||
|
#
|
||||||
|
# 'inbat' indoor battery level (1 normal, 0 low)
|
||||||
|
# 't1bat': outdoor battery level (1 normal, 0 low)
|
||||||
|
# 't234c1bat': CH2 battery level (1 normal, 0 low) CH2 in integration is CH1 in WSLink
|
||||||
|
|
||||||
|
|
||||||
DISABLED_BY_DEFAULT: Final = [
|
DISABLED_BY_DEFAULT: Final = [
|
||||||
CH2_TEMP,
|
CH2_TEMP,
|
||||||
CH2_HUMIDITY,
|
CH2_HUMIDITY,
|
||||||
CH2_BATTERY,
|
|
||||||
CH3_TEMP,
|
CH3_TEMP,
|
||||||
CH3_HUMIDITY,
|
CH3_HUMIDITY,
|
||||||
CH4_TEMP,
|
CH4_TEMP,
|
||||||
CH4_HUMIDITY,
|
CH4_HUMIDITY,
|
||||||
OUTSIDE_BATTERY,
|
|
||||||
WBGT_TEMP,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
BATTERY_LIST = [
|
|
||||||
OUTSIDE_BATTERY,
|
|
||||||
INDOOR_BATTERY,
|
|
||||||
CH2_BATTERY,
|
|
||||||
]
|
|
||||||
|
|
||||||
BATTERY_NON_BINARY: list[str] = [T9_BATTERY]
|
|
||||||
|
|
||||||
CONNECTION_GATED_SENSORS: Final[dict[str, list[str]]] = {
|
|
||||||
"t9cn": [HCHO, VOC, T9_BATTERY],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class VOCLevel(StrEnum):
|
|
||||||
"""WSLink VOC Level 1-5 (1-worst)."""
|
|
||||||
|
|
||||||
UNHEALTHY = "unhealthy"
|
|
||||||
POOR = "poor"
|
|
||||||
MODERATE = "moderate"
|
|
||||||
GOOD = "good"
|
|
||||||
EXCELLENT = "excellent"
|
|
||||||
|
|
||||||
|
|
||||||
VOC_LEVEL_MAP: dict[int, VOCLevel] = {
|
|
||||||
1: VOCLevel.UNHEALTHY,
|
|
||||||
2: VOCLevel.POOR,
|
|
||||||
3: VOCLevel.MODERATE,
|
|
||||||
4: VOCLevel.GOOD,
|
|
||||||
5: VOCLevel.EXCELLENT,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class UnitOfDir(StrEnum):
|
class UnitOfDir(StrEnum):
|
||||||
"""Wind direrction azimut."""
|
"""Wind direrction azimut."""
|
||||||
|
|
@ -261,18 +195,3 @@ AZIMUT: list[UnitOfDir] = [
|
||||||
UnitOfDir.NNW,
|
UnitOfDir.NNW,
|
||||||
UnitOfDir.N,
|
UnitOfDir.N,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class UnitOfBat(StrEnum):
|
|
||||||
"""Battery level unit of measure."""
|
|
||||||
|
|
||||||
LOW = "low"
|
|
||||||
NORMAL = "normal"
|
|
||||||
UNKNOWN = "unknown"
|
|
||||||
|
|
||||||
|
|
||||||
BATTERY_LEVEL: list[UnitOfBat] = [
|
|
||||||
UnitOfBat.LOW,
|
|
||||||
UnitOfBat.NORMAL,
|
|
||||||
UnitOfBat.UNKNOWN,
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,15 @@
|
||||||
{
|
{
|
||||||
"domain": "sws12500",
|
"domain": "sws12500",
|
||||||
"name": "Sencor SWS 12500 Weather Station",
|
"name": "Sencor SWS 12500 Weather Station",
|
||||||
"codeowners": [
|
"codeowners": ["@schizza"],
|
||||||
"@schizza"
|
|
||||||
],
|
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": [
|
"dependencies": ["http"],
|
||||||
"http"
|
|
||||||
],
|
|
||||||
"documentation": "https://github.com/schizza/SWS-12500-custom-component",
|
"documentation": "https://github.com/schizza/SWS-12500-custom-component",
|
||||||
"homekit": {},
|
"homekit": {},
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"issue_tracker": "https://github.com/schizza/SWS-12500-custom-component/issues",
|
"issue_tracker": "https://github.com/schizza/SWS-12500-custom-component/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"ssdp": [],
|
"ssdp": [],
|
||||||
"version": "1.8.5",
|
"version": "1.6.2",
|
||||||
"zeroconf": []
|
"zeroconf": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
"""Pocasi CZ resend functions."""
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import logging
|
|
||||||
from typing import Any, Literal
|
|
||||||
|
|
||||||
from aiohttp import ClientError
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
DEFAULT_URL,
|
|
||||||
POCASI_CZ_API_ID,
|
|
||||||
POCASI_CZ_API_KEY,
|
|
||||||
POCASI_CZ_ENABLED,
|
|
||||||
POCASI_CZ_LOGGER_ENABLED,
|
|
||||||
POCASI_CZ_SEND_INTERVAL,
|
|
||||||
POCASI_CZ_SUCCESS,
|
|
||||||
POCASI_CZ_UNEXPECTED,
|
|
||||||
POCASI_CZ_URL,
|
|
||||||
POCASI_INVALID_KEY,
|
|
||||||
WSLINK_URL,
|
|
||||||
)
|
|
||||||
from .utils import update_options
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class PocasiNotInserted(Exception):
|
|
||||||
"""NotInserted state."""
|
|
||||||
|
|
||||||
|
|
||||||
class PocasiSuccess(Exception):
|
|
||||||
"""WindySucces state."""
|
|
||||||
|
|
||||||
|
|
||||||
class PocasiApiKeyError(Exception):
|
|
||||||
"""Windy API Key error."""
|
|
||||||
|
|
||||||
|
|
||||||
class PocasiPush:
|
|
||||||
"""Push data to Windy."""
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
|
|
||||||
"""Init."""
|
|
||||||
self.hass = hass
|
|
||||||
self.config = config
|
|
||||||
self._interval = int(self.config.options.get(POCASI_CZ_SEND_INTERVAL, 30))
|
|
||||||
|
|
||||||
self.last_update = datetime.now()
|
|
||||||
self.next_update = datetime.now() + timedelta(seconds=self._interval)
|
|
||||||
|
|
||||||
self.log = self.config.options.get(POCASI_CZ_LOGGER_ENABLED)
|
|
||||||
self.invalid_response_count = 0
|
|
||||||
|
|
||||||
def verify_response(
|
|
||||||
self,
|
|
||||||
response: str,
|
|
||||||
) -> PocasiNotInserted | PocasiSuccess | PocasiApiKeyError | None:
|
|
||||||
"""Verify answer form server."""
|
|
||||||
|
|
||||||
if self.log:
|
|
||||||
_LOGGER.debug("Pocasi CZ responded: %s", response)
|
|
||||||
|
|
||||||
# Server does not provide any responses.
|
|
||||||
# This is placeholder if future state is changed
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def push_data_to_server(
|
|
||||||
self, data: dict[str, Any], mode: Literal["WU", "WSLINK"]
|
|
||||||
):
|
|
||||||
"""Pushes weather data to server."""
|
|
||||||
|
|
||||||
_data = data.copy()
|
|
||||||
_api_id = self.config.options.get(POCASI_CZ_API_ID)
|
|
||||||
_api_key = self.config.options.get(POCASI_CZ_API_KEY)
|
|
||||||
|
|
||||||
if self.log:
|
|
||||||
_LOGGER.info(
|
|
||||||
"Pocasi CZ last update = %s, next update at: %s",
|
|
||||||
str(self.last_update),
|
|
||||||
str(self.next_update),
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.next_update > datetime.now():
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Triggered update interval limit of %s seconds. Next possilbe update is set to: %s",
|
|
||||||
self._interval,
|
|
||||||
self.next_update,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
request_url: str = ""
|
|
||||||
if mode == "WSLINK":
|
|
||||||
_data["wsid"] = _api_id
|
|
||||||
_data["wspw"] = _api_key
|
|
||||||
request_url = f"{POCASI_CZ_URL}{WSLINK_URL}"
|
|
||||||
|
|
||||||
if mode == "WU":
|
|
||||||
_data["ID"] = _api_id
|
|
||||||
_data["PASSWORD"] = _api_key
|
|
||||||
request_url = f"{POCASI_CZ_URL}{DEFAULT_URL}"
|
|
||||||
|
|
||||||
session = async_get_clientsession(self.hass, verify_ssl=False)
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Payload for Pocasi Meteo server: [mode=%s] [request_url=%s] = %s",
|
|
||||||
mode,
|
|
||||||
request_url,
|
|
||||||
_data,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
async with session.get(request_url, params=_data) as resp:
|
|
||||||
status = await resp.text()
|
|
||||||
try:
|
|
||||||
self.verify_response(status)
|
|
||||||
|
|
||||||
except PocasiApiKeyError:
|
|
||||||
# log despite of settings
|
|
||||||
_LOGGER.critical(POCASI_INVALID_KEY)
|
|
||||||
await update_options(
|
|
||||||
self.hass, self.config, POCASI_CZ_ENABLED, False
|
|
||||||
)
|
|
||||||
except PocasiSuccess:
|
|
||||||
if self.log:
|
|
||||||
_LOGGER.info(POCASI_CZ_SUCCESS)
|
|
||||||
|
|
||||||
except ClientError as ex:
|
|
||||||
_LOGGER.critical("Invalid response from Pocasi Meteo: %s", str(ex))
|
|
||||||
self.invalid_response_count += 1
|
|
||||||
if self.invalid_response_count > 3:
|
|
||||||
_LOGGER.critical(POCASI_CZ_UNEXPECTED)
|
|
||||||
await update_options(self.hass, self.config, POCASI_CZ_ENABLED, False)
|
|
||||||
|
|
||||||
self.last_update = datetime.now()
|
|
||||||
self.next_update = datetime.now() + timedelta(seconds=self._interval)
|
|
||||||
|
|
||||||
if self.log:
|
|
||||||
_LOGGER.info("Next update: %s", str(self.next_update))
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"""Store routes info."""
|
"""Store routes info."""
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
|
|
@ -15,7 +14,7 @@ class Route:
|
||||||
|
|
||||||
url_path: str
|
url_path: str
|
||||||
route: AbstractRoute
|
route: AbstractRoute
|
||||||
handler: Callable
|
handler: callable
|
||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
@ -30,11 +29,11 @@ class Routes:
|
||||||
"""Initialize routes."""
|
"""Initialize routes."""
|
||||||
self.routes = {}
|
self.routes = {}
|
||||||
|
|
||||||
def switch_route(self, coordinator: Callable, url_path: str):
|
def switch_route(self, coordinator: callable, url_path: str):
|
||||||
"""Switch route."""
|
"""Switch route."""
|
||||||
|
|
||||||
for route in self.routes.values():
|
for url, route in self.routes.items():
|
||||||
if route.url_path == url_path:
|
if url == url_path:
|
||||||
_LOGGER.info("New coordinator to route: %s", route.url_path)
|
_LOGGER.info("New coordinator to route: %s", route.url_path)
|
||||||
route.enabled = True
|
route.enabled = True
|
||||||
route.handler = coordinator
|
route.handler = coordinator
|
||||||
|
|
@ -48,24 +47,22 @@ class Routes:
|
||||||
self,
|
self,
|
||||||
url_path: str,
|
url_path: str,
|
||||||
route: AbstractRoute,
|
route: AbstractRoute,
|
||||||
handler: Callable,
|
handler: callable,
|
||||||
enabled: bool = False,
|
enabled: bool = False,
|
||||||
):
|
):
|
||||||
"""Add route."""
|
"""Add route."""
|
||||||
key = f"{route.method}:{url_path}"
|
self.routes[url_path] = Route(url_path, route, handler, enabled)
|
||||||
self.routes[key] = Route(url_path, route, handler, enabled)
|
|
||||||
|
|
||||||
def get_route(self, url_path: str) -> Route | None:
|
def get_route(self, url_path: str) -> Route:
|
||||||
"""Get route."""
|
"""Get route."""
|
||||||
for route in self.routes.values():
|
return self.routes.get(url_path)
|
||||||
if route.url_path == url_path:
|
|
||||||
return route
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_enabled(self) -> str:
|
def get_enabled(self) -> str:
|
||||||
"""Get enabled routes."""
|
"""Get enabled routes."""
|
||||||
enabled_routes = {route.url_path for route in self.routes.values() if route.enabled}
|
enabled_routes = [
|
||||||
return ", ".join(sorted(enabled_routes)) if enabled_routes else "None"
|
route.url_path for route in self.routes.values() if route.enabled
|
||||||
|
]
|
||||||
|
return "".join(enabled_routes) if enabled_routes else "None"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return string representation."""
|
"""Return string representation."""
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
from homeassistant.components.sensor import RestoreSensor, SensorEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||||
|
|
@ -12,7 +12,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import WeatherDataUpdateCoordinator
|
from . import WeatherDataUpdateCoordinator
|
||||||
from .const import (
|
from .const import (
|
||||||
BATTERY_LIST,
|
|
||||||
CHILL_INDEX,
|
CHILL_INDEX,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
HEAT_INDEX,
|
HEAT_INDEX,
|
||||||
|
|
@ -23,12 +22,11 @@ from .const import (
|
||||||
WIND_DIR,
|
WIND_DIR,
|
||||||
WIND_SPEED,
|
WIND_SPEED,
|
||||||
WSLINK,
|
WSLINK,
|
||||||
UnitOfBat,
|
|
||||||
)
|
)
|
||||||
from .sensors_common import WeatherSensorEntityDescription
|
from .sensors_common import WeatherSensorEntityDescription
|
||||||
from .sensors_weather import SENSOR_TYPES_WEATHER_API
|
from .sensors_weather import SENSOR_TYPES_WEATHER_API
|
||||||
from .sensors_wslink import SENSOR_TYPES_WSLINK
|
from .sensors_wslink import SENSOR_TYPES_WSLINK
|
||||||
from .utils import battery_level_to_icon, battery_level_to_text, chill_index, heat_index
|
from .utils import chill_index, heat_index
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -44,12 +42,12 @@ async def async_setup_entry(
|
||||||
|
|
||||||
sensors_to_load: list = []
|
sensors_to_load: list = []
|
||||||
sensors: list = []
|
sensors: list = []
|
||||||
_wslink = config_entry.options.get(WSLINK)
|
_wslink = config_entry.data.get(WSLINK)
|
||||||
|
|
||||||
SENSOR_TYPES = SENSOR_TYPES_WSLINK if _wslink else SENSOR_TYPES_WEATHER_API
|
SENSOR_TYPES = SENSOR_TYPES_WSLINK if _wslink else SENSOR_TYPES_WEATHER_API
|
||||||
|
|
||||||
# Check if we have some sensors to load.
|
# Check if we have some sensors to load.
|
||||||
if sensors_to_load := config_entry.options.get(SENSORS_TO_LOAD, []):
|
if sensors_to_load := config_entry.options.get(SENSORS_TO_LOAD):
|
||||||
if WIND_DIR in sensors_to_load:
|
if WIND_DIR in sensors_to_load:
|
||||||
sensors_to_load.append(WIND_AZIMUT)
|
sensors_to_load.append(WIND_AZIMUT)
|
||||||
if (OUTSIDE_HUMIDITY in sensors_to_load) and (OUTSIDE_TEMP in sensors_to_load):
|
if (OUTSIDE_HUMIDITY in sensors_to_load) and (OUTSIDE_TEMP in sensors_to_load):
|
||||||
|
|
@ -65,9 +63,9 @@ async def async_setup_entry(
|
||||||
async_add_entities(sensors)
|
async_add_entities(sensors)
|
||||||
|
|
||||||
|
|
||||||
class WeatherSensor( # pyright: ignore[reportIncompatibleVariableOverride]
|
class WeatherSensor(
|
||||||
CoordinatorEntity[WeatherDataUpdateCoordinator], SensorEntity
|
CoordinatorEntity[WeatherDataUpdateCoordinator], RestoreSensor, SensorEntity
|
||||||
): # pyright: ignore[reportIncompatibleVariableOverride]
|
):
|
||||||
"""Implementation of Weather Sensor entity."""
|
"""Implementation of Weather Sensor entity."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
@ -94,6 +92,12 @@ class WeatherSensor( # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
self.coordinator.async_add_listener(self._handle_coordinator_update)
|
self.coordinator.async_add_listener(self._handle_coordinator_update)
|
||||||
|
|
||||||
|
# prev_state_data = await self.async_get_last_sensor_data()
|
||||||
|
# prev_state = await self.async_get_last_state()
|
||||||
|
# if not prev_state:
|
||||||
|
# return
|
||||||
|
# self._data = prev_state_data.native_value
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the coordinator."""
|
||||||
|
|
@ -104,31 +108,29 @@ class WeatherSensor( # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self): # pyright: ignore[reportIncompatibleVariableOverride]
|
def native_value(self) -> str | int | float | None:
|
||||||
"""Return value of entity."""
|
"""Return value of entity."""
|
||||||
|
|
||||||
_wslink = self.coordinator.config.options.get(WSLINK)
|
_wslink = self.coordinator.config.options.get(WSLINK)
|
||||||
|
|
||||||
if self.coordinator.data and (WIND_AZIMUT in self.entity_description.key):
|
if self.coordinator.data and (WIND_AZIMUT in self.entity_description.key):
|
||||||
return self.entity_description.value_fn(self.coordinator.data.get(WIND_DIR)) # pyright: ignore[ reportAttributeAccessIssue]
|
return self.entity_description.value_fn(self.coordinator.data.get(WIND_DIR))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.coordinator.data
|
self.coordinator.data
|
||||||
and (HEAT_INDEX in self.entity_description.key)
|
and (HEAT_INDEX in self.entity_description.key)
|
||||||
and not _wslink
|
and not _wslink
|
||||||
):
|
):
|
||||||
return self.entity_description.value_fn(heat_index(self.coordinator.data)) # pyright: ignore[ reportAttributeAccessIssue]
|
return self.entity_description.value_fn(heat_index(self.coordinator.data))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.coordinator.data
|
self.coordinator.data
|
||||||
and (CHILL_INDEX in self.entity_description.key)
|
and (CHILL_INDEX in self.entity_description.key)
|
||||||
and not _wslink
|
and not _wslink
|
||||||
):
|
):
|
||||||
return self.entity_description.value_fn(chill_index(self.coordinator.data)) # pyright: ignore[ reportAttributeAccessIssue]
|
return self.entity_description.value_fn(chill_index(self.coordinator.data))
|
||||||
|
|
||||||
return (
|
return None if self._data == "" else self.entity_description.value_fn(self._data)
|
||||||
None if self._data == "" else self.entity_description.value_fn(self._data) # pyright: ignore[ reportAttributeAccessIssue]
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def suggested_entity_id(self) -> str:
|
def suggested_entity_id(self) -> str:
|
||||||
|
|
@ -136,20 +138,7 @@ class WeatherSensor( # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
return generate_entity_id("sensor.{}", self.entity_description.key)
|
return generate_entity_id("sensor.{}", self.entity_description.key)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str | None: # pyright: ignore[reportIncompatibleVariableOverride]
|
def device_info(self) -> DeviceInfo:
|
||||||
"""Return the dynamic icon for battery representation."""
|
|
||||||
|
|
||||||
if self.entity_description.key in BATTERY_LIST:
|
|
||||||
if self.native_value:
|
|
||||||
battery_level = battery_level_to_text(self.native_value)
|
|
||||||
return battery_level_to_icon(battery_level)
|
|
||||||
|
|
||||||
return battery_level_to_icon(UnitOfBat.UNKNOWN)
|
|
||||||
|
|
||||||
return self.entity_description.icon
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> DeviceInfo: # pyright: ignore[reportIncompatibleVariableOverride]
|
|
||||||
"""Device info."""
|
"""Device info."""
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
connections=set(),
|
connections=set(),
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
|
||||||
WeatherSensorEntityDescription(
|
WeatherSensorEntityDescription(
|
||||||
key=WIND_DIR,
|
key=WIND_DIR,
|
||||||
native_unit_of_measurement=DEGREE,
|
native_unit_of_measurement=DEGREE,
|
||||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
|
||||||
suggested_display_precision=None,
|
suggested_display_precision=None,
|
||||||
icon="mdi:sign-direction",
|
icon="mdi:sign-direction",
|
||||||
translation_key=WIND_DIR,
|
translation_key=WIND_DIR,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ from typing import cast
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
|
||||||
DEGREE,
|
DEGREE,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
UV_INDEX,
|
UV_INDEX,
|
||||||
|
|
@ -18,41 +17,35 @@ from homeassistant.const import (
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
BARO_PRESSURE,
|
BARO_PRESSURE,
|
||||||
CH2_BATTERY,
|
|
||||||
CH2_HUMIDITY,
|
CH2_HUMIDITY,
|
||||||
CH2_TEMP,
|
CH2_TEMP,
|
||||||
CH3_HUMIDITY,
|
CH3_HUMIDITY,
|
||||||
CH3_TEMP,
|
CH3_TEMP,
|
||||||
|
CH4_HUMIDITY,
|
||||||
|
CH4_TEMP,
|
||||||
CHILL_INDEX,
|
CHILL_INDEX,
|
||||||
DAILY_RAIN,
|
DAILY_RAIN,
|
||||||
DEW_POINT,
|
DEW_POINT,
|
||||||
HCHO,
|
|
||||||
HEAT_INDEX,
|
HEAT_INDEX,
|
||||||
HOURLY_RAIN,
|
|
||||||
INDOOR_BATTERY,
|
|
||||||
INDOOR_HUMIDITY,
|
INDOOR_HUMIDITY,
|
||||||
INDOOR_TEMP,
|
INDOOR_TEMP,
|
||||||
MONTHLY_RAIN,
|
|
||||||
OUTSIDE_BATTERY,
|
|
||||||
OUTSIDE_HUMIDITY,
|
OUTSIDE_HUMIDITY,
|
||||||
OUTSIDE_TEMP,
|
OUTSIDE_TEMP,
|
||||||
RAIN,
|
RAIN,
|
||||||
SOLAR_RADIATION,
|
SOLAR_RADIATION,
|
||||||
T9_BATTERY,
|
|
||||||
UV,
|
UV,
|
||||||
VOC,
|
|
||||||
WBGT_TEMP,
|
|
||||||
WEEKLY_RAIN,
|
|
||||||
WIND_AZIMUT,
|
WIND_AZIMUT,
|
||||||
WIND_DIR,
|
WIND_DIR,
|
||||||
WIND_GUST,
|
WIND_GUST,
|
||||||
WIND_SPEED,
|
WIND_SPEED,
|
||||||
YEARLY_RAIN,
|
|
||||||
UnitOfDir,
|
UnitOfDir,
|
||||||
VOCLevel,
|
MONTHLY_RAIN,
|
||||||
|
YEARLY_RAIN,
|
||||||
|
HOURLY_RAIN,
|
||||||
|
WEEKLY_RAIN,
|
||||||
)
|
)
|
||||||
from .sensors_common import WeatherSensorEntityDescription
|
from .sensors_common import WeatherSensorEntityDescription
|
||||||
from .utils import battery_5step_to_pct, voc_level_to_text, wind_dir_to_text
|
from .utils import wind_dir_to_text
|
||||||
|
|
||||||
SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
|
SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
|
||||||
WeatherSensorEntityDescription(
|
WeatherSensorEntityDescription(
|
||||||
|
|
@ -133,8 +126,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
|
||||||
WeatherSensorEntityDescription(
|
WeatherSensorEntityDescription(
|
||||||
key=WIND_DIR,
|
key=WIND_DIR,
|
||||||
native_unit_of_measurement=DEGREE,
|
native_unit_of_measurement=DEGREE,
|
||||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
|
||||||
suggested_display_precision=None,
|
suggested_display_precision=None,
|
||||||
icon="mdi:sign-direction",
|
icon="mdi:sign-direction",
|
||||||
translation_key=WIND_DIR,
|
translation_key=WIND_DIR,
|
||||||
|
|
@ -151,8 +143,8 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
|
||||||
WeatherSensorEntityDescription(
|
WeatherSensorEntityDescription(
|
||||||
key=RAIN,
|
key=RAIN,
|
||||||
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
||||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
device_class=SensorDeviceClass.PRECIPITATION,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.TOTAL,
|
||||||
suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
icon="mdi:weather-pouring",
|
icon="mdi:weather-pouring",
|
||||||
|
|
@ -251,25 +243,25 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
|
||||||
translation_key=CH2_HUMIDITY,
|
translation_key=CH2_HUMIDITY,
|
||||||
value_fn=lambda data: cast("int", data),
|
value_fn=lambda data: cast("int", data),
|
||||||
),
|
),
|
||||||
WeatherSensorEntityDescription(
|
# WeatherSensorEntityDescription(
|
||||||
key=CH3_TEMP,
|
# key=CH3_TEMP,
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
# native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
# state_class=SensorStateClass.MEASUREMENT,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
# device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
# suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
icon="mdi:weather-sunny",
|
# icon="mdi:weather-sunny",
|
||||||
translation_key=CH3_TEMP,
|
# translation_key=CH3_TEMP,
|
||||||
value_fn=lambda data: cast("float", data),
|
# value_fn=lambda data: cast(float, data),
|
||||||
),
|
# ),
|
||||||
WeatherSensorEntityDescription(
|
# WeatherSensorEntityDescription(
|
||||||
key=CH3_HUMIDITY,
|
# key=CH3_HUMIDITY,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
# native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
# state_class=SensorStateClass.MEASUREMENT,
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
# device_class=SensorDeviceClass.HUMIDITY,
|
||||||
icon="mdi:weather-sunny",
|
# icon="mdi:weather-sunny",
|
||||||
translation_key=CH3_HUMIDITY,
|
# translation_key=CH3_HUMIDITY,
|
||||||
value_fn=lambda data: cast("int", data),
|
# value_fn=lambda data: cast(int, data),
|
||||||
),
|
# ),
|
||||||
# WeatherSensorEntityDescription(
|
# WeatherSensorEntityDescription(
|
||||||
# key=CH4_TEMP,
|
# key=CH4_TEMP,
|
||||||
# native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
# native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||||
|
|
@ -311,61 +303,4 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
|
||||||
translation_key=CHILL_INDEX,
|
translation_key=CHILL_INDEX,
|
||||||
value_fn=lambda data: cast("int", data),
|
value_fn=lambda data: cast("int", data),
|
||||||
),
|
),
|
||||||
WeatherSensorEntityDescription(
|
|
||||||
key=OUTSIDE_BATTERY,
|
|
||||||
translation_key=OUTSIDE_BATTERY,
|
|
||||||
icon="mdi:battery-unknown",
|
|
||||||
device_class=SensorDeviceClass.ENUM,
|
|
||||||
value_fn=lambda data: data,
|
|
||||||
),
|
|
||||||
WeatherSensorEntityDescription(
|
|
||||||
key=CH2_BATTERY,
|
|
||||||
translation_key=CH2_BATTERY,
|
|
||||||
icon="mdi:battery-unknown",
|
|
||||||
device_class=SensorDeviceClass.ENUM,
|
|
||||||
value_fn=lambda data: data,
|
|
||||||
),
|
|
||||||
WeatherSensorEntityDescription(
|
|
||||||
key=INDOOR_BATTERY,
|
|
||||||
translation_key=INDOOR_BATTERY,
|
|
||||||
icon="mdi:battery-unknown",
|
|
||||||
device_class=SensorDeviceClass.ENUM,
|
|
||||||
value_fn=lambda data: data,
|
|
||||||
),
|
|
||||||
WeatherSensorEntityDescription(
|
|
||||||
key=WBGT_TEMP,
|
|
||||||
translation_key=WBGT_TEMP,
|
|
||||||
icon="mdi:thermometer",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
suggested_display_precision=2,
|
|
||||||
value_fn=lambda data: cast("int", data),
|
|
||||||
),
|
|
||||||
WeatherSensorEntityDescription(
|
|
||||||
key=HCHO,
|
|
||||||
translation_key=HCHO,
|
|
||||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
icon="mdi:molecule",
|
|
||||||
value_fn=lambda data: cast("int", data),
|
|
||||||
),
|
|
||||||
WeatherSensorEntityDescription(
|
|
||||||
key=VOC,
|
|
||||||
translation_key=VOC,
|
|
||||||
device_class=SensorDeviceClass.ENUM,
|
|
||||||
options=list(VOCLevel),
|
|
||||||
icon="mdi:air-filter",
|
|
||||||
value_fn=lambda data: cast("str", voc_level_to_text(data)),
|
|
||||||
),
|
|
||||||
WeatherSensorEntityDescription(
|
|
||||||
key=T9_BATTERY,
|
|
||||||
translation_key=T9_BATTERY,
|
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
suggested_display_precision=0,
|
|
||||||
value_fn=battery_5step_to_pct,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"valid_credentials_key": "Provide valid API KEY.",
|
"valid_credentials_key": "Provide valid API KEY.",
|
||||||
"valid_credentials_match": "API ID and API KEY should not be the same."
|
"valid_credentials_match": "API ID and API KEY should not be the same."
|
||||||
},
|
},
|
||||||
|
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"options": {
|
"options": {
|
||||||
"error": {
|
"error": {
|
||||||
"valid_credentials_api": "Provide valid API ID.",
|
"valid_credentials_api": "Provide valid API ID.",
|
||||||
|
|
@ -31,6 +33,7 @@
|
||||||
"valid_credentials_match": "API ID and API KEY should not be the same.",
|
"valid_credentials_match": "API ID and API KEY should not be the same.",
|
||||||
"windy_key_required": "Windy API key is required if you want to enable this function."
|
"windy_key_required": "Windy API key is required if you want to enable this function."
|
||||||
},
|
},
|
||||||
|
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Configure SWS12500 Integration",
|
"title": "Configure SWS12500 Integration",
|
||||||
|
|
@ -40,6 +43,7 @@
|
||||||
"windy": "Windy configuration"
|
"windy": "Windy configuration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"basic": {
|
"basic": {
|
||||||
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
||||||
"title": "Configure credentials",
|
"title": "Configure credentials",
|
||||||
|
|
@ -56,6 +60,7 @@
|
||||||
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
|
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"windy": {
|
"windy": {
|
||||||
"description": "Resend weather data to your Windy stations.",
|
"description": "Resend weather data to your Windy stations.",
|
||||||
"title": "Configure Windy",
|
"title": "Configure Windy",
|
||||||
|
|
@ -69,24 +74,6 @@
|
||||||
"windy_logger_checkbox": "Enable only if you want to send debuging data to the developer."
|
"windy_logger_checkbox": "Enable only if you want to send debuging data to the developer."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pocasi": {
|
|
||||||
"description": "Resend data to Pocasi Meteo CZ",
|
|
||||||
"title": "Configure Pocasi Meteo CZ",
|
|
||||||
"data": {
|
|
||||||
"POCASI_CZ_API_ID": "ID from your Pocasi Meteo APP",
|
|
||||||
"POCASI_CZ_API_KEY": "Key from your Pocasi Meteo APP",
|
|
||||||
"POCASI_CZ_SEND_INTERVAL": "Resend interval in seconds",
|
|
||||||
"pocasi_enabled_checkbox": "Enable resending data to Pocasi Meteo",
|
|
||||||
"pocasi_logger_checkbox": "Log data and responses"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"POCASI_CZ_API_ID": "You can obtain your ID in Pocasi Meteo App",
|
|
||||||
"POCASI_CZ_API_KEY": "You can obtain your KEY in Pocasi Meteo App",
|
|
||||||
"POCASI_CZ_SEND_INTERVAL": "Resend interval in seconds (minimum 12s, default 30s)",
|
|
||||||
"pocasi_enabled_checkbox": "Enables resending data to Pocasi Meteo",
|
|
||||||
"pocasi_logger_checkbox": "Enable only if you want to send debbug data to the developer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"migration": {
|
"migration": {
|
||||||
"title": "Statistic migration.",
|
"title": "Statistic migration.",
|
||||||
"description": "For the correct functioning of long-term statistics, it is necessary to migrate the sensor unit in the long-term statistics. The original unit of long-term statistics for daily precipitation was in mm/d, however, the station only sends data in mm without time differentiation.\n\n The sensor to be migrated is for daily precipitation. If the correct value is already in the list for the daily precipitation sensor (mm), then the migration is already complete.\n\n Migration result for the sensor: {migration_status}, a total of {migration_count} rows converted.",
|
"description": "For the correct functioning of long-term statistics, it is necessary to migrate the sensor unit in the long-term statistics. The original unit of long-term statistics for daily precipitation was in mm/d, however, the station only sends data in mm without time differentiation.\n\n The sensor to be migrated is for daily precipitation. If the correct value is already in the list for the daily precipitation sensor (mm), then the migration is already complete.\n\n Migration result for the sensor: {migration_status}, a total of {migration_count} rows converted.",
|
||||||
|
|
@ -101,71 +88,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"indoor_temp": {
|
"indoor_temp": { "name": "Indoor temperature" },
|
||||||
"name": "Indoor temperature"
|
"indoor_humidity": { "name": "Indoor humidity" },
|
||||||
},
|
"outside_temp": { "name": "Outside Temperature" },
|
||||||
"indoor_humidity": {
|
"outside_humidity": { "name": "Outside humidity" },
|
||||||
"name": "Indoor humidity"
|
"uv": { "name": "UV index" },
|
||||||
},
|
"baro_pressure": { "name": "Barometric pressure" },
|
||||||
"outside_temp": {
|
"dew_point": { "name": "Dew point" },
|
||||||
"name": "Outside Temperature"
|
"wind_speed": { "name": "Wind speed" },
|
||||||
},
|
"wind_dir": { "name": "Wind direction" },
|
||||||
"outside_humidity": {
|
"wind_gust": { "name": "Wind gust" },
|
||||||
"name": "Outside humidity"
|
"rain": { "name": "Rain" },
|
||||||
},
|
"daily_rain": { "name": "Daily precipitation" },
|
||||||
"uv": {
|
"solar_radiation": { "name": "Solar irradiance" },
|
||||||
"name": "UV index"
|
"ch2_temp": { "name": "Channel 2 temperature" },
|
||||||
},
|
"ch2_humidity": { "name": "Channel 2 humidity" },
|
||||||
"baro_pressure": {
|
"ch3_temp": { "name": "Channel 3 temperature" },
|
||||||
"name": "Barometric pressure"
|
"ch3_humidity": { "name": "Channel 3 humidity" },
|
||||||
},
|
"ch4_temp": { "name": "Channel 4 temperature" },
|
||||||
"dew_point": {
|
"ch4_humidity": { "name": "Channel 4 humidity" },
|
||||||
"name": "Dew point"
|
"heat_index": { "name": "Apparent temperature" },
|
||||||
},
|
"chill_index": { "name": "Wind chill" },
|
||||||
"wind_speed": {
|
|
||||||
"name": "Wind speed"
|
|
||||||
},
|
|
||||||
"wind_dir": {
|
|
||||||
"name": "Wind direction"
|
|
||||||
},
|
|
||||||
"wind_gust": {
|
|
||||||
"name": "Wind gust"
|
|
||||||
},
|
|
||||||
"rain": {
|
|
||||||
"name": "Rain"
|
|
||||||
},
|
|
||||||
"daily_rain": {
|
|
||||||
"name": "Daily precipitation"
|
|
||||||
},
|
|
||||||
"solar_radiation": {
|
|
||||||
"name": "Solar irradiance"
|
|
||||||
},
|
|
||||||
"ch2_temp": {
|
|
||||||
"name": "Channel 2 temperature"
|
|
||||||
},
|
|
||||||
"ch2_humidity": {
|
|
||||||
"name": "Channel 2 humidity"
|
|
||||||
},
|
|
||||||
"ch3_temp": {
|
|
||||||
"name": "Channel 3 temperature"
|
|
||||||
},
|
|
||||||
"ch3_humidity": {
|
|
||||||
"name": "Channel 3 humidity"
|
|
||||||
},
|
|
||||||
"ch4_temp": {
|
|
||||||
"name": "Channel 4 temperature"
|
|
||||||
},
|
|
||||||
"ch4_humidity": {
|
|
||||||
"name": "Channel 4 humidity"
|
|
||||||
},
|
|
||||||
"heat_index": {
|
|
||||||
"name": "Apparent temperature"
|
|
||||||
},
|
|
||||||
"chill_index": {
|
|
||||||
"name": "Wind chill"
|
|
||||||
},
|
|
||||||
"wind_azimut": {
|
"wind_azimut": {
|
||||||
"name": "Bearing",
|
"name": "Bearing",
|
||||||
"state": {
|
"state": {
|
||||||
|
|
@ -185,14 +131,6 @@
|
||||||
"wnw": "WNW",
|
"wnw": "WNW",
|
||||||
"nw": "NW",
|
"nw": "NW",
|
||||||
"nnw": "NNW"
|
"nnw": "NNW"
|
||||||
},
|
|
||||||
"outside_battery": {
|
|
||||||
"name": "Outside battery level",
|
|
||||||
"state": {
|
|
||||||
"normal": "OK",
|
|
||||||
"low": "Low",
|
|
||||||
"unknown": "Unknown / drained out"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,17 +24,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"options": {
|
"options": {
|
||||||
"error": {
|
"error": {
|
||||||
"valid_credentials_api": "Vyplňte platné API ID",
|
"valid_credentials_api": "Vyplňte platné API ID",
|
||||||
"valid_credentials_key": "Vyplňte platný API KEY",
|
"valid_credentials_key": "Vyplňte platný API KEY",
|
||||||
"valid_credentials_match": "API ID a API KEY nesmějí být stejné!",
|
"valid_credentials_match": "API ID a API KEY nesmějí být stejné!",
|
||||||
"windy_id_required": "Je vyžadováno Windy ID, pokud chcete aktivovat přeposílání dat na Windy",
|
"windy_key_required": "Je vyžadován Windy API key, pokud chcete aktivovat přeposílání dat na Windy"
|
||||||
"windy_pw_required": "Je vyžadován Windy KEY, pokud chcete aktivovat přeposílání dat na Windy",
|
|
||||||
"pocasi_id_required": "Je vyžadován Počasí ID, pokud chcete aktivovat přeposílání dat na Počasí Meteo CZ",
|
|
||||||
"pocasi_key_required": "Klíč k účtu Počasí Meteo je povinný.",
|
|
||||||
"pocasi_send_minimum": "Minimální interval pro přeposílání je 12 sekund."
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Nastavení integrace SWS12500",
|
"title": "Nastavení integrace SWS12500",
|
||||||
|
|
@ -42,10 +40,10 @@
|
||||||
"menu_options": {
|
"menu_options": {
|
||||||
"basic": "Základní - přístupové údaje (přihlášení)",
|
"basic": "Základní - přístupové údaje (přihlášení)",
|
||||||
"windy": "Nastavení pro přeposílání dat na Windy",
|
"windy": "Nastavení pro přeposílání dat na Windy",
|
||||||
"pocasi": "Nastavení pro přeposlání dat na Počasí Meteo CZ",
|
|
||||||
"migration": "Migrace statistiky senzoru"
|
"migration": "Migrace statistiky senzoru"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"basic": {
|
"basic": {
|
||||||
"description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem",
|
"description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem",
|
||||||
"title": "Nastavení přihlášení",
|
"title": "Nastavení přihlášení",
|
||||||
|
|
@ -62,39 +60,20 @@
|
||||||
"wslink": "WSLink API zapněte, pokud je stanice nastavena na zasílání dat přes WSLink."
|
"wslink": "WSLink API zapněte, pokud je stanice nastavena na zasílání dat přes WSLink."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"windy": {
|
"windy": {
|
||||||
"description": "Přeposílání dat z metostanice na Windy",
|
"description": "Přeposílání dat z metostanice na Windy",
|
||||||
"title": "Konfigurace Windy",
|
"title": "Konfigurace Windy",
|
||||||
"data": {
|
"data": {
|
||||||
"WINDY_STATION_ID": "ID stanice, získaný z Windy",
|
"WINDY_API_KEY": "Klíč API KEY získaný z Windy",
|
||||||
"WINDY_STATION_PWD": "Heslo stanice, získané z Windy",
|
|
||||||
"windy_enabled_checkbox": "Povolit přeposílání dat na Windy",
|
"windy_enabled_checkbox": "Povolit přeposílání dat na Windy",
|
||||||
"windy_logger_checkbox": "Logovat data a odpovědi z Windy"
|
"windy_logger_checkbox": "Logovat data a odpovědi z Windy"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"WINDY_STATION_ID": "ID stanice získaný z https://stations.windy.com/station",
|
"WINDY_API_KEY": "Klíč API KEY získaný z https://https://api.windy.com/keys",
|
||||||
"WINDY_STATION_PWD": "Heslo stanice získané z https://stations.windy.com/station",
|
|
||||||
"windy_logger_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři."
|
"windy_logger_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pocasi": {
|
|
||||||
"description": "Přeposílání dat do aplikace Počasí Meteo",
|
|
||||||
"title": "Konfigurace Počasí Meteo",
|
|
||||||
"data": {
|
|
||||||
"POCASI_CZ_API_ID": "ID účtu na Počasí Meteo",
|
|
||||||
"POCASI_CZ_API_KEY": "Klíč (Key) k účtu Počasí Meteo",
|
|
||||||
"POCASI_CZ_SEND_INTERVAL": "Interval v sekundách",
|
|
||||||
"pocasi_enabled_chcekbox": "Povolit přeposílání dat na server Počasí Meteo",
|
|
||||||
"pocasi_logger_checkbox": "Logovat data a odpovědi z Počasí Meteo"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"POCASI_API_ID": "ID získáte ve své aplikaci Počasí Meteo",
|
|
||||||
"POCASI_API_KEY": "Klíč (Key) získáte ve své aplikaci Počasí Meteo",
|
|
||||||
"POCASI_SEND_INTERVAL": "Interval v jakém se mají data na server přeposílat (minimum 12s, defaultně 30s)",
|
|
||||||
"pocasi_enabled_checkbox": "Zapne přeposílání data na server Počasí Meteo",
|
|
||||||
"pocasi_logger_checkbox": "Zapnout pouze v případě, že chcete zaslat ladící informace vývojáři."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"migration": {
|
"migration": {
|
||||||
"title": "Migrace statistiky senzoru.",
|
"title": "Migrace statistiky senzoru.",
|
||||||
"description": "Pro správnou funkci dlouhodobé statistiky je nutné provést migraci jednotky senzoru v dlouhodobé statistice. Původní jednotka dlouhodobé statistiky pro denní úhrn srážek byla v mm/d, nicméně stanice zasílá pouze data v mm bez časového rozlišení.\n\n Senzor, který má být migrován je pro denní úhrn srážek. Pokud je v seznamu již správná hodnota u senzoru pro denní úhrn (mm), pak je již migrace hotová.\n\n Výsledek migrace pro senzor: {migration_status}, přepvedeno celkem {migration_count} řádků.",
|
"description": "Pro správnou funkci dlouhodobé statistiky je nutné provést migraci jednotky senzoru v dlouhodobé statistice. Původní jednotka dlouhodobé statistiky pro denní úhrn srážek byla v mm/d, nicméně stanice zasílá pouze data v mm bez časového rozlišení.\n\n Senzor, který má být migrován je pro denní úhrn srážek. Pokud je v seznamu již správná hodnota u senzoru pro denní úhrn (mm), pak je již migrace hotová.\n\n Výsledek migrace pro senzor: {migration_status}, přepvedeno celkem {migration_count} řádků.",
|
||||||
|
|
@ -109,102 +88,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"indoor_temp": {
|
"indoor_temp": { "name": "Vnitřní teplota" },
|
||||||
"name": "Vnitřní teplota"
|
"indoor_humidity": { "name": "Vnitřní vlhkost vzduchu" },
|
||||||
},
|
"outside_temp": { "name": "Venkovní teplota" },
|
||||||
"indoor_humidity": {
|
"outside_humidity": { "name": "Venkovní vlhkost vzduchu" },
|
||||||
"name": "Vnitřní vlhkost vzduchu"
|
"uv": { "name": "UV index" },
|
||||||
},
|
"baro_pressure": { "name": "Tlak vzduchu" },
|
||||||
"outside_temp": {
|
"dew_point": { "name": "Rosný bod" },
|
||||||
"name": "Venkovní teplota"
|
"wind_speed": { "name": "Rychlost větru" },
|
||||||
},
|
"wind_dir": { "name": "Směr větru" },
|
||||||
"outside_humidity": {
|
"wind_gust": { "name": "Poryvy větru" },
|
||||||
"name": "Venkovní vlhkost vzduchu"
|
"rain": { "name": "Srážky" },
|
||||||
},
|
"daily_rain": { "name": "Denní úhrn srážek" },
|
||||||
"uv": {
|
"solar_radiation": { "name": "Sluneční osvit" },
|
||||||
"name": "UV index"
|
"ch2_temp": { "name": "Teplota senzoru 2" },
|
||||||
},
|
"ch2_humidity": { "name": "Vlhkost sensoru 2" },
|
||||||
"baro_pressure": {
|
"ch3_temp": { "name": "Teplota senzoru 3" },
|
||||||
"name": "Tlak vzduchu"
|
"ch3_humidity": { "name": "Vlhkost sensoru 3" },
|
||||||
},
|
"ch4_temp": { "name": "Teplota senzoru 4" },
|
||||||
"dew_point": {
|
"ch4_humidity": { "name": "Vlhkost sensoru 4" },
|
||||||
"name": "Rosný bod"
|
"heat_index": { "name": "Tepelný index" },
|
||||||
},
|
"chill_index": { "name": "Pocitová teplota" },
|
||||||
"wind_speed": {
|
"hourly_rain": { "name": "Hodinový úhrn srážek" },
|
||||||
"name": "Rychlost větru"
|
"weekly_rain": { "name": "Týdenní úhrn srážek" },
|
||||||
},
|
"monthly_rain": { "name": "Měsíční úhrn srážek" },
|
||||||
"wind_dir": {
|
"yearly_rain": { "name": "Roční úhrn srážek" },
|
||||||
"name": "Směr větru"
|
|
||||||
},
|
|
||||||
"wind_gust": {
|
|
||||||
"name": "Poryvy větru"
|
|
||||||
},
|
|
||||||
"rain": {
|
|
||||||
"name": "Srážky"
|
|
||||||
},
|
|
||||||
"daily_rain": {
|
|
||||||
"name": "Denní úhrn srážek"
|
|
||||||
},
|
|
||||||
"solar_radiation": {
|
|
||||||
"name": "Sluneční osvit"
|
|
||||||
},
|
|
||||||
"ch2_temp": {
|
|
||||||
"name": "Teplota senzoru 2"
|
|
||||||
},
|
|
||||||
"ch2_humidity": {
|
|
||||||
"name": "Vlhkost sensoru 2"
|
|
||||||
},
|
|
||||||
"ch3_temp": {
|
|
||||||
"name": "Teplota senzoru 3"
|
|
||||||
},
|
|
||||||
"ch3_humidity": {
|
|
||||||
"name": "Vlhkost sensoru 3"
|
|
||||||
},
|
|
||||||
"ch4_temp": {
|
|
||||||
"name": "Teplota senzoru 4"
|
|
||||||
},
|
|
||||||
"ch4_humidity": {
|
|
||||||
"name": "Vlhkost sensoru 4"
|
|
||||||
},
|
|
||||||
"heat_index": {
|
|
||||||
"name": "Tepelný index"
|
|
||||||
},
|
|
||||||
"chill_index": {
|
|
||||||
"name": "Pocitová teplota"
|
|
||||||
},
|
|
||||||
"hourly_rain": {
|
|
||||||
"name": "Hodinový úhrn srážek"
|
|
||||||
},
|
|
||||||
"weekly_rain": {
|
|
||||||
"name": "Týdenní úhrn srážek"
|
|
||||||
},
|
|
||||||
"monthly_rain": {
|
|
||||||
"name": "Měsíční úhrn srážek"
|
|
||||||
},
|
|
||||||
"yearly_rain": {
|
|
||||||
"name": "Roční úhrn srážek"
|
|
||||||
},
|
|
||||||
"wbgt_temp": {
|
|
||||||
"name": "WBGT index"
|
|
||||||
},
|
|
||||||
"hcho": {
|
|
||||||
"name": "Formaldehyd (HCHO)"
|
|
||||||
},
|
|
||||||
"voc": {
|
|
||||||
"name": "Úroveň VOC",
|
|
||||||
"state": {
|
|
||||||
"unhealthy": "Nezdravá",
|
|
||||||
"poor": "Špatná",
|
|
||||||
"moderate": "Průměrná",
|
|
||||||
"good": "Dobrá",
|
|
||||||
"excellent": "Velmi dobrá"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"t9_battery": {
|
|
||||||
"name": "Baterie senzoru HCHO/VOC"
|
|
||||||
},
|
|
||||||
"wind_azimut": {
|
"wind_azimut": {
|
||||||
"name": "Azimut",
|
"name": "Azimut",
|
||||||
"state": {
|
"state": {
|
||||||
|
|
@ -225,30 +136,6 @@
|
||||||
"nw": "SZ",
|
"nw": "SZ",
|
||||||
"nnw": "SSZ"
|
"nnw": "SSZ"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"outside_battery": {
|
|
||||||
"name": "Stav nabití venkovní baterie",
|
|
||||||
"state": {
|
|
||||||
"low": "Nízká",
|
|
||||||
"normal": "Normální",
|
|
||||||
"unknown": "Neznámá / zcela vybitá"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indoor_battery": {
|
|
||||||
"name": "Stav nabití baterie kozole",
|
|
||||||
"state": {
|
|
||||||
"low": "Nízká",
|
|
||||||
"normal": "Normální",
|
|
||||||
"unknown": "Neznámá / zcela vybitá"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ch2_battery": {
|
|
||||||
"name": "Stav nabití baterie kanálu 2",
|
|
||||||
"state": {
|
|
||||||
"low": "Nízká",
|
|
||||||
"normal": "Normální",
|
|
||||||
"unknown": "Neznámá / zcela vybitá"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"valid_credentials_key": "Provide valid API KEY.",
|
"valid_credentials_key": "Provide valid API KEY.",
|
||||||
"valid_credentials_match": "API ID and API KEY should not be the same."
|
"valid_credentials_match": "API ID and API KEY should not be the same."
|
||||||
},
|
},
|
||||||
|
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
||||||
|
|
@ -24,14 +25,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"options": {
|
"options": {
|
||||||
"error": {
|
"error": {
|
||||||
"valid_credentials_api": "Provide valid API ID.",
|
"valid_credentials_api": "Provide valid API ID.",
|
||||||
"valid_credentials_key": "Provide valid API KEY.",
|
"valid_credentials_key": "Provide valid API KEY.",
|
||||||
"valid_credentials_match": "API ID and API KEY should not be the same.",
|
"valid_credentials_match": "API ID and API KEY should not be the same.",
|
||||||
"windy_id_required": "Windy API key is required if you want to enable this function.",
|
"windy_key_required": "Windy API key is required if you want to enable this function."
|
||||||
"windy_pw_required": "Windy API password is required if you want to enable this function."
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Configure SWS12500 Integration",
|
"title": "Configure SWS12500 Integration",
|
||||||
|
|
@ -41,6 +43,7 @@
|
||||||
"windy": "Windy configuration"
|
"windy": "Windy configuration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"basic": {
|
"basic": {
|
||||||
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
||||||
"title": "Configure credentials",
|
"title": "Configure credentials",
|
||||||
|
|
@ -57,39 +60,20 @@
|
||||||
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
|
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"windy": {
|
"windy": {
|
||||||
"description": "Resend weather data to your Windy stations.",
|
"description": "Resend weather data to your Windy stations.",
|
||||||
"title": "Configure Windy",
|
"title": "Configure Windy",
|
||||||
"data": {
|
"data": {
|
||||||
"WINDY_STATION_ID": "Station ID obtained form Windy",
|
"WINDY_API_KEY": "API KEY provided by Windy",
|
||||||
"WINDY_STATION_PWD": "Station password obtained from Windy",
|
|
||||||
"windy_enabled_checkbox": "Enable resending data to Windy",
|
"windy_enabled_checkbox": "Enable resending data to Windy",
|
||||||
"windy_logger_checkbox": "Log Windy data and responses"
|
"windy_logger_checkbox": "Log Windy data and responses"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"WINDY_STATION_ID": "Windy station ID obtained from https://stations.windy.com/stations",
|
"WINDY_API_KEY": "Windy API KEY obtained from https://https://api.windy.com/keys",
|
||||||
"WINDY_STATION_PWD": "Windy station password obtained from https://stations.windy.com/stations",
|
|
||||||
"windy_logger_checkbox": "Enable only if you want to send debuging data to the developer."
|
"windy_logger_checkbox": "Enable only if you want to send debuging data to the developer."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pocasi": {
|
|
||||||
"description": "Resend data to Pocasi Meteo CZ",
|
|
||||||
"title": "Configure Pocasi Meteo CZ",
|
|
||||||
"data": {
|
|
||||||
"POCASI_CZ_API_ID": "ID from your Pocasi Meteo APP",
|
|
||||||
"POCASI_CZ_API_KEY": "Key from your Pocasi Meteo APP",
|
|
||||||
"POCASI_CZ_SEND_INTERVAL": "Resend interval in seconds",
|
|
||||||
"pocasi_enabled_checkbox": "Enable resending data to Pocasi Meteo",
|
|
||||||
"pocasi_logger_checkbox": "Log data and responses"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"POCASI_CZ_API_ID": "You can obtain your ID in Pocasi Meteo App",
|
|
||||||
"POCASI_CZ_API_KEY": "You can obtain your KEY in Pocasi Meteo App",
|
|
||||||
"POCASI_CZ_SEND_INTERVAL": "Resend interval in seconds (minimum 12s, default 30s)",
|
|
||||||
"pocasi_enabled_checkbox": "Enables resending data to Pocasi Meteo",
|
|
||||||
"pocasi_logger_checkbox": "Enable only if you want to send debbug data to the developer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"migration": {
|
"migration": {
|
||||||
"title": "Statistic migration.",
|
"title": "Statistic migration.",
|
||||||
"description": "For the correct functioning of long-term statistics, it is necessary to migrate the sensor unit in the long-term statistics. The original unit of long-term statistics for daily precipitation was in mm/d, however, the station only sends data in mm without time differentiation.\n\n The sensor to be migrated is for daily precipitation. If the correct value is already in the list for the daily precipitation sensor (mm), then the migration is already complete.\n\n Migration result for the sensor: {migration_status}, a total of {migration_count} rows converted.",
|
"description": "For the correct functioning of long-term statistics, it is necessary to migrate the sensor unit in the long-term statistics. The original unit of long-term statistics for daily precipitation was in mm/d, however, the station only sends data in mm without time differentiation.\n\n The sensor to be migrated is for daily precipitation. If the correct value is already in the list for the daily precipitation sensor (mm), then the migration is already complete.\n\n Migration result for the sensor: {migration_status}, a total of {migration_count} rows converted.",
|
||||||
|
|
@ -104,102 +88,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"indoor_temp": {
|
"indoor_temp": { "name": "Indoor temperature" },
|
||||||
"name": "Indoor temperature"
|
"indoor_humidity": { "name": "Indoor humidity" },
|
||||||
},
|
"outside_temp": { "name": "Outside Temperature" },
|
||||||
"indoor_humidity": {
|
"outside_humidity": { "name": "Outside humidity" },
|
||||||
"name": "Indoor humidity"
|
"uv": { "name": "UV index" },
|
||||||
},
|
"baro_pressure": { "name": "Barometric pressure" },
|
||||||
"outside_temp": {
|
"dew_point": { "name": "Dew point" },
|
||||||
"name": "Outside Temperature"
|
"wind_speed": { "name": "Wind speed" },
|
||||||
},
|
"wind_dir": { "name": "Wind direction" },
|
||||||
"outside_humidity": {
|
"wind_gust": { "name": "Wind gust" },
|
||||||
"name": "Outside humidity"
|
"rain": { "name": "Rain" },
|
||||||
},
|
"daily_rain": { "name": "Daily precipitation" },
|
||||||
"uv": {
|
"solar_radiation": { "name": "Solar irradiance" },
|
||||||
"name": "UV index"
|
"ch2_temp": { "name": "Channel 2 temperature" },
|
||||||
},
|
"ch2_humidity": { "name": "Channel 2 humidity" },
|
||||||
"baro_pressure": {
|
"ch3_temp": { "name": "Channel 3 temperature" },
|
||||||
"name": "Barometric pressure"
|
"ch3_humidity": { "name": "Channel 3 humidity" },
|
||||||
},
|
"ch4_temp": { "name": "Channel 4 temperature" },
|
||||||
"dew_point": {
|
"ch4_humidity": { "name": "Channel 4 humidity" },
|
||||||
"name": "Dew point"
|
"heat_index": { "name": "Apparent temperature" },
|
||||||
},
|
"chill_index": { "name": "Wind chill" },
|
||||||
"wind_speed": {
|
"hourly_rain": { "name": "Hourly precipitation" },
|
||||||
"name": "Wind speed"
|
"weekly_rain": { "name": "Weekly precipitation" },
|
||||||
},
|
"monthly_rain": { "name": "Monthly precipitation" },
|
||||||
"wind_dir": {
|
"yearly_rain": { "name": "Yearly precipitation" },
|
||||||
"name": "Wind direction"
|
|
||||||
},
|
|
||||||
"wind_gust": {
|
|
||||||
"name": "Wind gust"
|
|
||||||
},
|
|
||||||
"rain": {
|
|
||||||
"name": "Rain"
|
|
||||||
},
|
|
||||||
"daily_rain": {
|
|
||||||
"name": "Daily precipitation"
|
|
||||||
},
|
|
||||||
"solar_radiation": {
|
|
||||||
"name": "Solar irradiance"
|
|
||||||
},
|
|
||||||
"ch2_temp": {
|
|
||||||
"name": "Channel 2 temperature"
|
|
||||||
},
|
|
||||||
"ch2_humidity": {
|
|
||||||
"name": "Channel 2 humidity"
|
|
||||||
},
|
|
||||||
"ch3_temp": {
|
|
||||||
"name": "Channel 3 temperature"
|
|
||||||
},
|
|
||||||
"ch3_humidity": {
|
|
||||||
"name": "Channel 3 humidity"
|
|
||||||
},
|
|
||||||
"ch4_temp": {
|
|
||||||
"name": "Channel 4 temperature"
|
|
||||||
},
|
|
||||||
"ch4_humidity": {
|
|
||||||
"name": "Channel 4 humidity"
|
|
||||||
},
|
|
||||||
"heat_index": {
|
|
||||||
"name": "Apparent temperature"
|
|
||||||
},
|
|
||||||
"chill_index": {
|
|
||||||
"name": "Wind chill"
|
|
||||||
},
|
|
||||||
"hourly_rain": {
|
|
||||||
"name": "Hourly precipitation"
|
|
||||||
},
|
|
||||||
"weekly_rain": {
|
|
||||||
"name": "Weekly precipitation"
|
|
||||||
},
|
|
||||||
"monthly_rain": {
|
|
||||||
"name": "Monthly precipitation"
|
|
||||||
},
|
|
||||||
"yearly_rain": {
|
|
||||||
"name": "Yearly precipitation"
|
|
||||||
},
|
|
||||||
"wbgt_index": {
|
|
||||||
"name": "WBGT index"
|
|
||||||
},
|
|
||||||
"hcho": {
|
|
||||||
"name": "Formaldehyde (HCHO)"
|
|
||||||
},
|
|
||||||
"voc": {
|
|
||||||
"name": "VOC level",
|
|
||||||
"state": {
|
|
||||||
"unhealthy": "Unhealthy",
|
|
||||||
"poor": "Poor",
|
|
||||||
"moderate": "Moderate",
|
|
||||||
"good": "Good",
|
|
||||||
"excellent": "Excellent"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"t9_battery": {
|
|
||||||
"name": "HCHO/VOC sensor battery"
|
|
||||||
},
|
|
||||||
"wind_azimut": {
|
"wind_azimut": {
|
||||||
"name": "Bearing",
|
"name": "Bearing",
|
||||||
"state": {
|
"state": {
|
||||||
|
|
@ -220,30 +136,6 @@
|
||||||
"nw": "NW",
|
"nw": "NW",
|
||||||
"nnw": "NNW"
|
"nnw": "NNW"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"outside_battery": {
|
|
||||||
"name": "Outside battery level",
|
|
||||||
"state": {
|
|
||||||
"normal": "OK",
|
|
||||||
"low": "Low",
|
|
||||||
"unknown": "Unknown / drained out"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ch2_battery": {
|
|
||||||
"name": "Channel 2 battery level",
|
|
||||||
"state": {
|
|
||||||
"normal": "OK",
|
|
||||||
"low": "Low",
|
|
||||||
"unknown": "Unknown / drained out"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indoor_battery": {
|
|
||||||
"name": "Console battery level",
|
|
||||||
"state": {
|
|
||||||
"normal": "OK",
|
|
||||||
"low": "Low",
|
|
||||||
"unknown": "Unknown / drained out"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,16 @@ import numpy as np
|
||||||
|
|
||||||
from homeassistant.components import persistent_notification
|
from homeassistant.components import persistent_notification
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import (
|
||||||
|
UnitOfPrecipitationDepth,
|
||||||
|
UnitOfTemperature,
|
||||||
|
UnitOfVolumetricFlux,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.translation import async_get_translations
|
from homeassistant.helpers.translation import async_get_translations
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
AZIMUT,
|
AZIMUT,
|
||||||
CONNECTION_GATED_SENSORS,
|
|
||||||
DATABASE_PATH,
|
DATABASE_PATH,
|
||||||
DEV_DBG,
|
DEV_DBG,
|
||||||
OUTSIDE_HUMIDITY,
|
OUTSIDE_HUMIDITY,
|
||||||
|
|
@ -23,11 +27,8 @@ from .const import (
|
||||||
REMAP_ITEMS,
|
REMAP_ITEMS,
|
||||||
REMAP_WSLINK_ITEMS,
|
REMAP_WSLINK_ITEMS,
|
||||||
SENSORS_TO_LOAD,
|
SENSORS_TO_LOAD,
|
||||||
VOC_LEVEL_MAP,
|
|
||||||
WIND_SPEED,
|
WIND_SPEED,
|
||||||
UnitOfBat,
|
|
||||||
UnitOfDir,
|
UnitOfDir,
|
||||||
VOCLevel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@ -47,10 +48,12 @@ async def translations(
|
||||||
|
|
||||||
language = hass.config.language
|
language = hass.config.language
|
||||||
|
|
||||||
_translations = await async_get_translations(hass, language, category, [translation_domain])
|
_translations = await async_get_translations(
|
||||||
|
hass, language, category, [translation_domain]
|
||||||
|
)
|
||||||
if localize_key in _translations:
|
if localize_key in _translations:
|
||||||
return _translations[localize_key]
|
return _translations[localize_key]
|
||||||
return ""
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def translated_notification(
|
async def translated_notification(
|
||||||
|
|
@ -62,16 +65,20 @@ async def translated_notification(
|
||||||
*,
|
*,
|
||||||
key: str = "message",
|
key: str = "message",
|
||||||
category: str = "notify",
|
category: str = "notify",
|
||||||
):
|
) -> str:
|
||||||
"""Translate notification."""
|
"""Translate notification."""
|
||||||
|
|
||||||
localize_key = f"component.{translation_domain}.{category}.{translation_key}.{key}"
|
localize_key = f"component.{translation_domain}.{category}.{translation_key}.{key}"
|
||||||
|
|
||||||
localize_title = f"component.{translation_domain}.{category}.{translation_key}.title"
|
localize_title = (
|
||||||
|
f"component.{translation_domain}.{category}.{translation_key}.title"
|
||||||
|
)
|
||||||
|
|
||||||
language = hass.config.language
|
language = hass.config.language
|
||||||
|
|
||||||
_translations = await async_get_translations(hass, language, category, [translation_domain])
|
_translations = await async_get_translations(
|
||||||
|
hass, language, category, [translation_domain]
|
||||||
|
)
|
||||||
if localize_key in _translations:
|
if localize_key in _translations:
|
||||||
if not translation_placeholders:
|
if not translation_placeholders:
|
||||||
persistent_notification.async_create(
|
persistent_notification.async_create(
|
||||||
|
|
@ -82,10 +89,14 @@ async def translated_notification(
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
message = _translations[localize_key].format(**translation_placeholders)
|
message = _translations[localize_key].format(**translation_placeholders)
|
||||||
persistent_notification.async_create(hass, message, _translations[localize_title], notification_id)
|
persistent_notification.async_create(
|
||||||
|
hass, message, _translations[localize_title], notification_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def update_options(hass: HomeAssistant, entry: ConfigEntry, update_key, update_value) -> bool:
|
async def update_options(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, update_key, update_value
|
||||||
|
) -> None:
|
||||||
"""Update config.options entry."""
|
"""Update config.options entry."""
|
||||||
conf = {**entry.options}
|
conf = {**entry.options}
|
||||||
conf[update_key] = update_value
|
conf[update_key] = update_value
|
||||||
|
|
@ -121,11 +132,6 @@ def remap_wslink_items(entities):
|
||||||
if item in REMAP_WSLINK_ITEMS:
|
if item in REMAP_WSLINK_ITEMS:
|
||||||
items[REMAP_WSLINK_ITEMS[item]] = entities[item]
|
items[REMAP_WSLINK_ITEMS[item]] = entities[item]
|
||||||
|
|
||||||
for conn_key, gated in CONNECTION_GATED_SENSORS.items():
|
|
||||||
if str(entities.get(conn_key, "0")) != "1":
|
|
||||||
for key in gated:
|
|
||||||
items.pop(key, None)
|
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -135,7 +141,9 @@ def loaded_sensors(config_entry: ConfigEntry) -> list | None:
|
||||||
return config_entry.options.get(SENSORS_TO_LOAD) or []
|
return config_entry.options.get(SENSORS_TO_LOAD) or []
|
||||||
|
|
||||||
|
|
||||||
def check_disabled(hass: HomeAssistant, items, config_entry: ConfigEntry) -> list | None:
|
def check_disabled(
|
||||||
|
hass: HomeAssistant, items, config_entry: ConfigEntry
|
||||||
|
) -> list | None:
|
||||||
"""Check if we have data for unloaded sensors.
|
"""Check if we have data for unloaded sensors.
|
||||||
|
|
||||||
If so, then add sensor to load queue.
|
If so, then add sensor to load queue.
|
||||||
|
|
@ -143,7 +151,7 @@ def check_disabled(hass: HomeAssistant, items, config_entry: ConfigEntry) -> lis
|
||||||
Returns list of found sensors or None
|
Returns list of found sensors or None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log: bool = config_entry.options.get(DEV_DBG, False)
|
log: bool = config_entry.options.get(DEV_DBG)
|
||||||
entityFound: bool = False
|
entityFound: bool = False
|
||||||
_loaded_sensors = loaded_sensors(config_entry)
|
_loaded_sensors = loaded_sensors(config_entry)
|
||||||
missing_sensors: list = []
|
missing_sensors: list = []
|
||||||
|
|
@ -173,37 +181,6 @@ def wind_dir_to_text(deg: float) -> UnitOfDir | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def battery_level_to_text(battery: int) -> UnitOfBat:
|
|
||||||
"""Return battery level in text representation.
|
|
||||||
|
|
||||||
Returns UnitOfBat
|
|
||||||
"""
|
|
||||||
|
|
||||||
level_map: dict[int, UnitOfBat] = {
|
|
||||||
0: UnitOfBat.LOW,
|
|
||||||
1: UnitOfBat.NORMAL,
|
|
||||||
}
|
|
||||||
|
|
||||||
if battery is None:
|
|
||||||
return UnitOfBat.UNKNOWN
|
|
||||||
|
|
||||||
return level_map.get(int(battery), UnitOfBat.UNKNOWN)
|
|
||||||
|
|
||||||
|
|
||||||
def battery_level_to_icon(battery: UnitOfBat) -> str:
|
|
||||||
"""Return battery level in icon representation.
|
|
||||||
|
|
||||||
Returns str
|
|
||||||
"""
|
|
||||||
|
|
||||||
icons = {
|
|
||||||
UnitOfBat.LOW: "mdi:battery-low",
|
|
||||||
UnitOfBat.NORMAL: "mdi:battery",
|
|
||||||
}
|
|
||||||
|
|
||||||
return icons.get(battery, "mdi:battery-unknown")
|
|
||||||
|
|
||||||
|
|
||||||
def fahrenheit_to_celsius(fahrenheit: float) -> float:
|
def fahrenheit_to_celsius(fahrenheit: float) -> float:
|
||||||
"""Convert Fahrenheit to Celsius."""
|
"""Convert Fahrenheit to Celsius."""
|
||||||
return (fahrenheit - 32) * 5.0 / 9.0
|
return (fahrenheit - 32) * 5.0 / 9.0
|
||||||
|
|
@ -214,22 +191,15 @@ def celsius_to_fahrenheit(celsius: float) -> float:
|
||||||
return celsius * 9.0 / 5.0 + 32
|
return celsius * 9.0 / 5.0 + 32
|
||||||
|
|
||||||
|
|
||||||
def heat_index(data: Any, convert: bool = False) -> float | None:
|
def heat_index(data: Any, convert: bool = False) -> UnitOfTemperature:
|
||||||
"""Calculate heat index from temperature.
|
"""Calculate heat index from temperature.
|
||||||
|
|
||||||
data: dict with temperature and humidity
|
data: dict with temperature and humidity
|
||||||
convert: bool, convert recieved data from Celsius to Fahrenheit
|
convert: bool, convert recieved data from Celsius to Fahrenheit
|
||||||
"""
|
"""
|
||||||
|
|
||||||
temp = data.get(OUTSIDE_TEMP, None)
|
temp = float(data[OUTSIDE_TEMP])
|
||||||
rh = data.get(OUTSIDE_HUMIDITY, None)
|
rh = float(data[OUTSIDE_HUMIDITY])
|
||||||
|
|
||||||
if not temp or not rh:
|
|
||||||
return None
|
|
||||||
|
|
||||||
temp = float(temp)
|
|
||||||
rh = float(rh)
|
|
||||||
|
|
||||||
adjustment = None
|
adjustment = None
|
||||||
|
|
||||||
if convert:
|
if convert:
|
||||||
|
|
@ -259,28 +229,26 @@ def heat_index(data: Any, convert: bool = False) -> float | None:
|
||||||
return simple
|
return simple
|
||||||
|
|
||||||
|
|
||||||
def chill_index(data: Any, convert: bool = False) -> float | None:
|
def chill_index(data: Any, convert: bool = False) -> UnitOfTemperature:
|
||||||
"""Calculate wind chill index from temperature and wind speed.
|
"""Calculate wind chill index from temperature and wind speed.
|
||||||
|
|
||||||
data: dict with temperature and wind speed
|
data: dict with temperature and wind speed
|
||||||
convert: bool, convert recieved data from Celsius to Fahrenheit
|
convert: bool, convert recieved data from Celsius to Fahrenheit
|
||||||
"""
|
"""
|
||||||
|
|
||||||
temp = data.get(OUTSIDE_TEMP, None)
|
temp = float(data[OUTSIDE_TEMP])
|
||||||
wind = data.get(WIND_SPEED, None)
|
wind = float(data[WIND_SPEED])
|
||||||
|
|
||||||
if not temp or not wind:
|
|
||||||
return None
|
|
||||||
|
|
||||||
temp = float(temp)
|
|
||||||
wind = float(wind)
|
|
||||||
|
|
||||||
if convert:
|
if convert:
|
||||||
temp = celsius_to_fahrenheit(temp)
|
temp = celsius_to_fahrenheit(temp)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
round(
|
round(
|
||||||
((35.7 + (0.6215 * temp)) - (35.75 * (wind**0.16)) + (0.4275 * (temp * (wind**0.16)))),
|
(
|
||||||
|
(35.7 + (0.6215 * temp))
|
||||||
|
- (35.75 * (wind**0.16))
|
||||||
|
+ (0.4275 * (temp * (wind**0.16)))
|
||||||
|
),
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
if temp < 50 and wind > 3
|
if temp < 50 and wind > 3
|
||||||
|
|
@ -288,25 +256,9 @@ def chill_index(data: Any, convert: bool = False) -> float | None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def voc_level_to_text(value: str) -> VOCLevel | None:
|
|
||||||
"""Map 1-5 VOC level to text state."""
|
|
||||||
if value in (None, ""):
|
|
||||||
return None
|
|
||||||
return VOC_LEVEL_MAP.get(int(value))
|
|
||||||
|
|
||||||
|
|
||||||
def battery_5step_to_pct(value: str) -> int | None:
|
|
||||||
"""Convert 0-5 battery steps to percentage."""
|
|
||||||
|
|
||||||
if value in (None, ""):
|
|
||||||
return None
|
|
||||||
|
|
||||||
return round(int(value) / 5 * 100)
|
|
||||||
|
|
||||||
|
|
||||||
def long_term_units_in_statistics_meta():
|
def long_term_units_in_statistics_meta():
|
||||||
"""Get units in long term statitstics."""
|
"""Get units in long term statitstics."""
|
||||||
sensor_units = []
|
|
||||||
if not Path(DATABASE_PATH).exists():
|
if not Path(DATABASE_PATH).exists():
|
||||||
_LOGGER.error("Database file not found: %s", DATABASE_PATH)
|
_LOGGER.error("Database file not found: %s", DATABASE_PATH)
|
||||||
return False
|
return False
|
||||||
|
|
@ -315,14 +267,14 @@ def long_term_units_in_statistics_meta():
|
||||||
db = conn.cursor()
|
db = conn.cursor()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.execute(
|
db.execute("""
|
||||||
"""
|
|
||||||
SELECT statistic_id, unit_of_measurement from statistics_meta
|
SELECT statistic_id, unit_of_measurement from statistics_meta
|
||||||
WHERE statistic_id LIKE 'sensor.weather_station_sws%'
|
WHERE statistic_id LIKE 'sensor.weather_station_sws%'
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
rows = db.fetchall()
|
rows = db.fetchall()
|
||||||
sensor_units = {statistic_id: f"{statistic_id} ({unit})" for statistic_id, unit in rows}
|
sensor_units = {
|
||||||
|
statistic_id: f"{statistic_id} ({unit})" for statistic_id, unit in rows
|
||||||
|
}
|
||||||
|
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
_LOGGER.error("Error during data migration: %s", e)
|
_LOGGER.error("Error during data migration: %s", e)
|
||||||
|
|
@ -332,7 +284,7 @@ def long_term_units_in_statistics_meta():
|
||||||
return sensor_units
|
return sensor_units
|
||||||
|
|
||||||
|
|
||||||
async def migrate_data(hass: HomeAssistant, sensor_id: str | None = None) -> int | bool:
|
async def migrate_data(hass: HomeAssistant, sensor_id: str | None = None) -> bool:
|
||||||
"""Migrate data from mm/d to mm."""
|
"""Migrate data from mm/d to mm."""
|
||||||
|
|
||||||
_LOGGER.debug("Sensor %s is required for data migration", sensor_id)
|
_LOGGER.debug("Sensor %s is required for data migration", sensor_id)
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,17 @@
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ClientError
|
|
||||||
|
|
||||||
from homeassistant.components import persistent_notification
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
PURGE_DATA,
|
PURGE_DATA,
|
||||||
|
WINDY_API_KEY,
|
||||||
WINDY_ENABLED,
|
WINDY_ENABLED,
|
||||||
WINDY_INVALID_KEY,
|
WINDY_INVALID_KEY,
|
||||||
WINDY_LOGGER_ENABLED,
|
WINDY_LOGGER_ENABLED,
|
||||||
WINDY_NOT_INSERTED,
|
WINDY_NOT_INSERTED,
|
||||||
WINDY_STATION_ID,
|
|
||||||
WINDY_STATION_PW,
|
|
||||||
WINDY_SUCCESS,
|
WINDY_SUCCESS,
|
||||||
WINDY_UNEXPECTED,
|
WINDY_UNEXPECTED,
|
||||||
WINDY_URL,
|
WINDY_URL,
|
||||||
|
|
@ -89,7 +85,7 @@ class WindyPush:
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def push_data_to_windy(self, data, wslink: bool = False):
|
async def push_data_to_windy(self, data):
|
||||||
"""Pushes weather data do Windy stations.
|
"""Pushes weather data do Windy stations.
|
||||||
|
|
||||||
Interval is 5 minutes, otherwise Windy would not accepts data.
|
Interval is 5 minutes, otherwise Windy would not accepts data.
|
||||||
|
|
@ -98,6 +94,8 @@ class WindyPush:
|
||||||
from station. But we need to do some clean up.
|
from station. But we need to do some clean up.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
text_for_test = None
|
||||||
|
|
||||||
if self.log:
|
if self.log:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Windy last update = %s, next update at: %s",
|
"Windy last update = %s, next update at: %s",
|
||||||
|
|
@ -108,66 +106,24 @@ class WindyPush:
|
||||||
if self.next_update > datetime.now():
|
if self.next_update > datetime.now():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
purged_data = data.copy()
|
purged_data = dict(data)
|
||||||
|
|
||||||
for purge in PURGE_DATA:
|
for purge in PURGE_DATA:
|
||||||
if purge in purged_data:
|
if purge in purged_data:
|
||||||
purged_data.pop(purge)
|
purged_data.pop(purge)
|
||||||
|
|
||||||
if wslink:
|
if "dewptf" in purged_data:
|
||||||
# WSLink -> Windy params
|
dewpoint = round(((float(purged_data.pop("dewptf")) - 32) / 1.8), 1)
|
||||||
if "t1ws" in purged_data:
|
purged_data["dewpoint"] = str(dewpoint)
|
||||||
purged_data["wind"] = purged_data.pop("t1ws")
|
|
||||||
if "t1wgust" in purged_data:
|
|
||||||
purged_data["gust"] = purged_data.pop("t1wgust")
|
|
||||||
if "t1wdir" in purged_data:
|
|
||||||
purged_data["winddir"] = purged_data.pop("t1wdir")
|
|
||||||
if "t1hum" in purged_data:
|
|
||||||
purged_data["humidity"] = purged_data.pop("t1hum")
|
|
||||||
if "t1dew" in purged_data:
|
|
||||||
purged_data["dewpoint"] = purged_data.pop("t1dew")
|
|
||||||
if "t1tem" in purged_data:
|
|
||||||
purged_data["temp"] = purged_data.pop("t1tem")
|
|
||||||
if "rbar" in purged_data:
|
|
||||||
purged_data["mbar"] = purged_data.pop("rbar")
|
|
||||||
if "t1rainhr" in purged_data:
|
|
||||||
purged_data["precip"] = purged_data.pop("t1rainhr")
|
|
||||||
if "t1uvi" in purged_data:
|
|
||||||
purged_data["uv"] = purged_data.pop("t1uvi")
|
|
||||||
if "t1solrad" in purged_data:
|
|
||||||
purged_data["solarradiation"] = purged_data.pop("t1solrad")
|
|
||||||
|
|
||||||
windy_station_id = (self.config.options.get(WINDY_STATION_ID) or "").strip()
|
windy_api_key = self.config.options.get(WINDY_API_KEY)
|
||||||
windy_station_pw = (self.config.options.get(WINDY_STATION_PW) or "").strip()
|
request_url = f"{WINDY_URL}{windy_api_key}"
|
||||||
|
|
||||||
# Both values are required. Options can sometimes be None, so normalize to
|
|
||||||
# empty string and strip whitespace before validating.
|
|
||||||
if not windy_station_id or not windy_station_pw:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Windy ID or PASSWORD is not set correctly. Please reconfigure your WINDY resend credentials. Disabling WINDY resend for now!"
|
|
||||||
)
|
|
||||||
|
|
||||||
persistent_notification.async_create(
|
|
||||||
self.hass,
|
|
||||||
"Your Windy credentials are not set correctly. Disabling Windy resending for now. Update Windy options and enable reseding.",
|
|
||||||
"Windy resending disabled.",
|
|
||||||
)
|
|
||||||
|
|
||||||
await update_options(self.hass, self.config, WINDY_ENABLED, False)
|
|
||||||
return False
|
|
||||||
|
|
||||||
request_url = f"{WINDY_URL}"
|
|
||||||
|
|
||||||
purged_data["id"] = windy_station_id
|
|
||||||
purged_data["time"] = "now"
|
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {windy_station_pw}"}
|
|
||||||
|
|
||||||
if self.log:
|
if self.log:
|
||||||
_LOGGER.info("Dataset for windy: %s", purged_data)
|
_LOGGER.info("Dataset for windy: %s", purged_data)
|
||||||
session = async_get_clientsession(self.hass, verify_ssl=False)
|
session = async_get_clientsession(self.hass, verify_ssl=False)
|
||||||
try:
|
try:
|
||||||
async with session.get(request_url, params=purged_data, headers=headers) as resp:
|
async with session.get(request_url, params=purged_data) as resp:
|
||||||
status = await resp.text()
|
status = await resp.text()
|
||||||
try:
|
try:
|
||||||
self.verify_windy_response(status)
|
self.verify_windy_response(status)
|
||||||
|
|
@ -175,22 +131,27 @@ class WindyPush:
|
||||||
# log despite of settings
|
# log despite of settings
|
||||||
_LOGGER.error(WINDY_NOT_INSERTED)
|
_LOGGER.error(WINDY_NOT_INSERTED)
|
||||||
|
|
||||||
|
text_for_test = WINDY_NOT_INSERTED
|
||||||
|
|
||||||
except WindyApiKeyError:
|
except WindyApiKeyError:
|
||||||
# log despite of settings
|
# log despite of settings
|
||||||
_LOGGER.critical(WINDY_INVALID_KEY)
|
_LOGGER.critical(WINDY_INVALID_KEY)
|
||||||
|
text_for_test = WINDY_INVALID_KEY
|
||||||
|
|
||||||
await update_options(self.hass, self.config, WINDY_ENABLED, False)
|
update_options(self.hass, self.config, WINDY_ENABLED, False)
|
||||||
|
|
||||||
except WindySuccess:
|
except WindySuccess:
|
||||||
if self.log:
|
if self.log:
|
||||||
_LOGGER.info(WINDY_SUCCESS)
|
_LOGGER.info(WINDY_SUCCESS)
|
||||||
|
text_for_test = WINDY_SUCCESS
|
||||||
|
|
||||||
except ClientError as ex:
|
except session.ClientError as ex:
|
||||||
_LOGGER.critical("Invalid response from Windy: %s", str(ex))
|
_LOGGER.critical("Invalid response from Windy: %s", str(ex))
|
||||||
self.invalid_response_count += 1
|
self.invalid_response_count += 1
|
||||||
if self.invalid_response_count > 3:
|
if self.invalid_response_count > 3:
|
||||||
_LOGGER.critical(WINDY_UNEXPECTED)
|
_LOGGER.critical(WINDY_UNEXPECTED)
|
||||||
await update_options(self.hass, self.config, WINDY_ENABLED, False)
|
text_for_test = WINDY_UNEXPECTED
|
||||||
|
update_options(self.hass, self.config, WINDY_ENABLED, False)
|
||||||
|
|
||||||
self.last_update = datetime.now()
|
self.last_update = datetime.now()
|
||||||
self.next_update = self.last_update + timed(minutes=5)
|
self.next_update = self.last_update + timed(minutes=5)
|
||||||
|
|
@ -198,4 +159,6 @@ class WindyPush:
|
||||||
if self.log:
|
if self.log:
|
||||||
_LOGGER.info("Next update: %s", str(self.next_update))
|
_LOGGER.info("Next update: %s", str(self.next_update))
|
||||||
|
|
||||||
|
if RESPONSE_FOR_TEST and text_for_test:
|
||||||
|
return text_for_test
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
"""Shared keys and helpers for storing integration runtime state in hass.data.
|
||||||
|
|
||||||
|
This integration uses `hass.data[DOMAIN][entry_id]` as a per-entry dictionary.
|
||||||
|
Keeping keys in one place prevents subtle bugs where different modules store
|
||||||
|
different value types under the same key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
|
||||||
|
# Per-entry dict keys stored under hass.data[DOMAIN][entry_id]
|
||||||
|
ENTRY_COORDINATOR: Final[str] = "coordinator"
|
||||||
|
ENTRY_ADD_ENTITIES: Final[str] = "async_add_entities"
|
||||||
|
ENTRY_DESCRIPTIONS: Final[str] = "sensor_descriptions"
|
||||||
|
ENTRY_LAST_OPTIONS: Final[str] = "last_options"
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
{
|
{
|
||||||
"name": "Sencor SWS 12500 Weather station",
|
"name": "Sencor SWS 12500 Weather station",
|
||||||
"filename": "weather-station.zip",
|
"render_readme": true
|
||||||
"render_readme": true,
|
|
||||||
"zip_release": true
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
"""Pytest configuration for tests under `dev/tests`.
|
|
||||||
|
|
||||||
Goals:
|
|
||||||
- Make `custom_components.*` importable.
|
|
||||||
- Keep this file lightweight and avoid global HA test-harness side effects.
|
|
||||||
|
|
||||||
Repository layout:
|
|
||||||
- Root custom components: `SWS-12500/custom_components/...` (symlinked to `dev/custom_components/...`)
|
|
||||||
- Integration sources: `SWS-12500/dev/custom_components/...`
|
|
||||||
|
|
||||||
Note:
|
|
||||||
Some tests use lightweight `hass` stubs (e.g. SimpleNamespace) that are not compatible with
|
|
||||||
Home Assistant's full test fixtures. Do NOT enable HA-only fixtures globally here.
|
|
||||||
Instead, request such fixtures (e.g. `enable_custom_integrations`) explicitly in the specific
|
|
||||||
tests that need HA's integration loader / flow managers.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure() -> None:
|
|
||||||
"""Adjust sys.path so imports and HA loader discovery work in tests."""
|
|
||||||
repo_root = Path(__file__).resolve().parents[2] # .../SWS-12500
|
|
||||||
dev_root = repo_root / "dev"
|
|
||||||
|
|
||||||
# Ensure the repo root is importable so HA can find `custom_components/<domain>/manifest.json`.
|
|
||||||
repo_root_str = str(repo_root)
|
|
||||||
if repo_root_str not in sys.path:
|
|
||||||
sys.path.insert(0, repo_root_str)
|
|
||||||
|
|
||||||
# Also ensure `dev/` is importable for direct imports from dev tooling/tests.
|
|
||||||
dev_root_str = str(dev_root)
|
|
||||||
if dev_root_str not in sys.path:
|
|
||||||
sys.path.insert(0, dev_root_str)
|
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
"""Tests for the T9 air-quality (HCHO / VOC) sensor support.
|
|
||||||
|
|
||||||
Covers what was added for the WSLink ``t9hcho`` / ``t9voclv`` / ``t9bat`` /
|
|
||||||
``t9cn`` parameters:
|
|
||||||
|
|
||||||
- the new constants (``REMAP_WSLINK_ITEMS``, ``CONNECTION_GATED_SENSORS``,
|
|
||||||
``BATTERY_NON_BINARY``, ``VOCLevel`` / ``VOC_LEVEL_MAP``)
|
|
||||||
- the ``utils.voc_level_to_text`` and ``utils.battery_5step_to_pct`` helpers
|
|
||||||
- the connection gating in ``utils.remap_wslink_items``
|
|
||||||
- the new ``SENSOR_TYPES_WSLINK`` entity descriptions
|
|
||||||
- the ``hcho`` / ``voc`` / ``t9_battery`` entries in the translation files
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from custom_components.sws12500.const import (
|
|
||||||
BATTERY_LIST,
|
|
||||||
BATTERY_NON_BINARY,
|
|
||||||
CONNECTION_GATED_SENSORS,
|
|
||||||
HCHO,
|
|
||||||
OUTSIDE_TEMP,
|
|
||||||
REMAP_WSLINK_ITEMS,
|
|
||||||
T9_BATTERY,
|
|
||||||
VOC,
|
|
||||||
VOC_LEVEL_MAP,
|
|
||||||
VOCLevel,
|
|
||||||
)
|
|
||||||
from custom_components.sws12500.sensors_wslink import SENSOR_TYPES_WSLINK
|
|
||||||
from custom_components.sws12500.utils import battery_5step_to_pct, remap_wslink_items, voc_level_to_text
|
|
||||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
|
||||||
from homeassistant.const import CONCENTRATION_PARTS_PER_BILLION, PERCENTAGE
|
|
||||||
|
|
||||||
# Realistic WSLink payload taken from an issue report: the station sends every
|
|
||||||
# parameter, even for channels with no sensor connected (``*cn == "0"``).
|
|
||||||
ISSUE_PAYLOAD = {
|
|
||||||
"rbar": "1013.3",
|
|
||||||
"intem": "25.0",
|
|
||||||
"inhum": "44",
|
|
||||||
"t1cn": "1",
|
|
||||||
"t1tem": "11.3",
|
|
||||||
"t1hum": "92",
|
|
||||||
"t234c1cn": "1",
|
|
||||||
"t234c2cn": "1",
|
|
||||||
"t234c3cn": "0",
|
|
||||||
"t8cn": "0",
|
|
||||||
"t9cn": "1",
|
|
||||||
"t9hcho": "57",
|
|
||||||
"t9voclv": "5",
|
|
||||||
"t9bat": "5",
|
|
||||||
"t10cn": "0",
|
|
||||||
"t11cn": "0",
|
|
||||||
"apiver": "1.00",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# --- constants -------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def test_t9_keys_are_remapped() -> None:
|
|
||||||
assert REMAP_WSLINK_ITEMS["t9hcho"] == HCHO
|
|
||||||
assert REMAP_WSLINK_ITEMS["t9voclv"] == VOC
|
|
||||||
assert REMAP_WSLINK_ITEMS["t9bat"] == T9_BATTERY
|
|
||||||
# t9cn is intentionally NOT remapped - it is only used as a gating flag.
|
|
||||||
assert "t9cn" not in REMAP_WSLINK_ITEMS
|
|
||||||
|
|
||||||
|
|
||||||
def test_connection_gated_sensors_definition() -> None:
|
|
||||||
assert CONNECTION_GATED_SENSORS == {"t9cn": [HCHO, VOC, T9_BATTERY]}
|
|
||||||
|
|
||||||
|
|
||||||
def test_t9_battery_is_non_binary_only() -> None:
|
|
||||||
assert BATTERY_NON_BINARY == [T9_BATTERY]
|
|
||||||
# the 0-5 / percentage battery must not be treated as a binary low/normal one
|
|
||||||
assert T9_BATTERY not in BATTERY_LIST
|
|
||||||
|
|
||||||
|
|
||||||
def test_voc_level_map_is_complete_and_ordered() -> None:
|
|
||||||
# 1 == highest VOC reading (worst air) ... 5 == lowest VOC reading (best air)
|
|
||||||
assert set(VOC_LEVEL_MAP) == {1, 2, 3, 4, 5}
|
|
||||||
assert set(VOC_LEVEL_MAP.values()) == set(VOCLevel)
|
|
||||||
assert VOC_LEVEL_MAP[1] is VOCLevel.UNHEALTHY
|
|
||||||
assert VOC_LEVEL_MAP[5] is VOCLevel.EXCELENT
|
|
||||||
assert [member.value for member in VOCLevel] == [
|
|
||||||
"unhealthy",
|
|
||||||
"poor",
|
|
||||||
"moderate",
|
|
||||||
"good",
|
|
||||||
"excellent",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# --- voc_level_to_text -----------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("empty", [None, ""])
|
|
||||||
def test_voc_level_to_text_handles_empty(empty) -> None:
|
|
||||||
assert voc_level_to_text(empty) is None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("raw", "expected"),
|
|
||||||
[
|
|
||||||
("1", VOCLevel.UNHEALTHY),
|
|
||||||
("2", VOCLevel.POOR),
|
|
||||||
("3", VOCLevel.MODERATE),
|
|
||||||
("4", VOCLevel.GOOD),
|
|
||||||
("5", VOCLevel.EXCELENT),
|
|
||||||
(3, VOCLevel.MODERATE),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_voc_level_to_text_maps_known_levels(raw, expected) -> None:
|
|
||||||
assert voc_level_to_text(raw) == expected
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("raw", ["0", "6", 0, 6])
|
|
||||||
def test_voc_level_to_text_out_of_range_is_none(raw) -> None:
|
|
||||||
assert voc_level_to_text(raw) is None
|
|
||||||
|
|
||||||
|
|
||||||
# --- battery_5step_to_pct --------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("empty", [None, ""])
|
|
||||||
def test_battery_5step_to_pct_handles_empty(empty) -> None:
|
|
||||||
assert battery_5step_to_pct(empty) is None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("raw", "expected"),
|
|
||||||
[("0", 0), ("1", 20), ("2", 40), ("3", 60), ("4", 80), ("5", 100), (5, 100)],
|
|
||||||
)
|
|
||||||
def test_battery_5step_to_pct_scales_to_percentage(raw, expected) -> None:
|
|
||||||
assert battery_5step_to_pct(raw) == expected
|
|
||||||
|
|
||||||
|
|
||||||
# --- remap_wslink_items connection gating ----------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def test_remap_keeps_t9_group_when_connected() -> None:
|
|
||||||
out = remap_wslink_items({"t9cn": "1", "t9hcho": "57", "t9voclv": "5", "t9bat": "5", "t1tem": "11.3"})
|
|
||||||
assert out[HCHO] == "57"
|
|
||||||
assert out[VOC] == "5"
|
|
||||||
assert out[T9_BATTERY] == "5"
|
|
||||||
assert out[OUTSIDE_TEMP] == "11.3"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("conn", [{"t9cn": "0"}, {}], ids=["disconnected", "absent"])
|
|
||||||
def test_remap_drops_t9_group_when_disconnected_or_absent(conn) -> None:
|
|
||||||
out = remap_wslink_items({**conn, "t9hcho": "57", "t9voclv": "5", "t9bat": "5", "t1tem": "11.3"})
|
|
||||||
assert HCHO not in out
|
|
||||||
assert VOC not in out
|
|
||||||
assert T9_BATTERY not in out
|
|
||||||
# unrelated sensors are untouched by the gating
|
|
||||||
assert out[OUTSIDE_TEMP] == "11.3"
|
|
||||||
|
|
||||||
|
|
||||||
def test_remap_issue_payload_exposes_t9_when_connected() -> None:
|
|
||||||
out = remap_wslink_items(ISSUE_PAYLOAD)
|
|
||||||
# t9cn == "1" -> the T9 sensors are exposed
|
|
||||||
assert out[HCHO] == "57"
|
|
||||||
assert out[VOC] == "5"
|
|
||||||
assert out[T9_BATTERY] == "5"
|
|
||||||
# connection flags never leak into the sensor data
|
|
||||||
assert "t9cn" not in out
|
|
||||||
assert "t9_conn" not in out
|
|
||||||
|
|
||||||
|
|
||||||
# --- sensor entity descriptions -------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def wslink_descriptions():
|
|
||||||
return {description.key: description for description in SENSOR_TYPES_WSLINK}
|
|
||||||
|
|
||||||
|
|
||||||
def test_hcho_entity_description(wslink_descriptions) -> None:
|
|
||||||
description = wslink_descriptions[HCHO]
|
|
||||||
assert description.translation_key == HCHO
|
|
||||||
assert description.device_class is SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
|
||||||
assert description.native_unit_of_measurement == CONCENTRATION_PARTS_PER_BILLION
|
|
||||||
assert description.state_class is SensorStateClass.MEASUREMENT
|
|
||||||
# value_fn is a pass-through (typing.cast is a no-op at runtime; HA coerces the str)
|
|
||||||
assert description.value_fn("57") == "57"
|
|
||||||
|
|
||||||
|
|
||||||
def test_voc_entity_description(wslink_descriptions) -> None:
|
|
||||||
description = wslink_descriptions[VOC]
|
|
||||||
assert description.translation_key == VOC
|
|
||||||
assert description.device_class is SensorDeviceClass.ENUM
|
|
||||||
assert description.options == list(VOCLevel)
|
|
||||||
# ENUM sensors must not declare a state_class
|
|
||||||
assert description.state_class is None
|
|
||||||
assert description.value_fn("1") == VOCLevel.UNHEALTHY
|
|
||||||
assert description.value_fn("5") == "excellent"
|
|
||||||
assert description.value_fn(None) is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_t9_battery_entity_description(wslink_descriptions) -> None:
|
|
||||||
description = wslink_descriptions[T9_BATTERY]
|
|
||||||
assert description.translation_key == T9_BATTERY
|
|
||||||
assert description.device_class is SensorDeviceClass.BATTERY
|
|
||||||
assert description.native_unit_of_measurement == PERCENTAGE
|
|
||||||
assert description.state_class is SensorStateClass.MEASUREMENT
|
|
||||||
assert description.suggested_display_precision == 0
|
|
||||||
# no explicit icon -> HA renders the battery icon from the device class + %
|
|
||||||
assert description.icon is None
|
|
||||||
assert description.value_fn("5") == 100
|
|
||||||
assert description.value_fn("0") == 0
|
|
||||||
assert description.value_fn(None) is None
|
|
||||||
|
|
||||||
|
|
||||||
# --- translation files -----------------------------------------------------
|
|
||||||
|
|
||||||
_TRANSLATIONS_DIR = Path(__file__).resolve().parents[1] / "custom_components" / "sws12500" / "translations"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("filename", ["en.json", "cs.json"])
|
|
||||||
def test_translation_files_have_t9_entries(filename) -> None:
|
|
||||||
sensors = json.loads((_TRANSLATIONS_DIR / filename).read_text(encoding="utf-8"))["entity"]["sensor"]
|
|
||||||
|
|
||||||
assert sensors["hcho"]["name"]
|
|
||||||
assert sensors["t9_battery"]["name"]
|
|
||||||
|
|
||||||
voc = sensors["voc"]
|
|
||||||
assert voc["name"]
|
|
||||||
assert set(voc["state"]) == {member.value for member in VOCLevel}
|
|
||||||
Loading…
Reference in New Issue