Compare commits

..

106 Commits

Author SHA1 Message Date
Lukas Svoboda ee7be37ba6 Add shared hass.data keys/constants for cleaner integration state storage (local) 2026-01-20 20:01:29 +01:00
Lukas Svoboda 8034ada12f Add shared hass.data keys/constants for cleaner integration state storage 2026-01-20 19:59:29 +01:00
schizza 99d25bfd56 Adds unit migration functionality to options flow
Implements a user interface to migrate units for rain sensors including migration of historic data via statistics.
This provides the user with the ability to correct rain units, if they have been set incorrectly.
Includes UI to select sensor and units, as well as trigger migration.
2025-04-24 17:56:47 +02:00
Lukas Svoboda af87fd0719
Merge pull request #69 from schizza/wslink_name_fix1
WSLINK_name_fix
2025-04-13 17:54:53 +02:00
schizza 0ab5321170 Corrects translation keys for rain sensors
Updates the translation keys for hourly, weekly, monthly, and yearly rain sensors to ensure they accurately reflect the corresponding time periods.
2025-04-13 17:52:58 +02:00
schizza fdcd28f96a Fix BUG 68
Fixing Issue #68
2025-04-04 14:18:23 +02:00
Lukas Svoboda 093a7915f0
Merge pull request #67 from schizza/add_sensors_for_wslink
Adds rain sensors for different time periods for WSLink
2025-04-04 13:15:01 +02:00
schizza ea0a5e34e3 Update version number 2025-04-04 13:14:19 +02:00
Lukas Svoboda fdd4cacddf
Merge branch 'main' into add_sensors_for_wslink 2025-04-04 13:11:45 +02:00
schizza 601d1f3984 Adds rain sensors for different time periods for WSLink
Adds hourly, weekly, monthly, and yearly rain sensors to the integration for WSLink connection.

The units of measurement are corrected for the rain sensor, now displaying millimeters per hour as the station is sending data in mm/h for 'rain' sensor. And cumulative precipitation is in millimeters.

Also includes translations for the new sensors.
2025-04-04 13:10:10 +02:00
Lukas Svoboda 287e74673e
Create config.yml 2025-03-29 11:30:34 +01:00
Lukas Svoboda 397e44e5f2
Update issue templates 2025-03-29 11:27:06 +01:00
Lukas Svoboda f9d80d7a00
Update bug_report.md 2025-03-19 18:25:13 +01:00
Lukas Svoboda 2b57eeb4fa
Create FUNDING.yml 2025-03-19 18:23:59 +01:00
Lukas Svoboda dc5c45483e
Update issue templates 2025-03-19 17:54:22 +01:00
Lukas Svoboda 3d112757d0
Update const.py
Update path to database as it differs in development installation.
2025-03-15 15:44:57 +01:00
Lukas Svoboda 189777fbdb
Merge pull request #63 from schizza/migrate_data
Fixes rainfall unit inconsistency

Updates daily rain sensor to use consistent measurement units and corrects device class and suggested unit.

Adds database path constant and data migration function

Defines DATABASE_PATH constant for database file location Introduces migrate_data function to update unit of measurement in long statistics.

Adds sensor migration feature

Introduces a migration step for sensor statistics
Updates schema and translations to support migration Fixes data migration from mm/d to mm
2025-03-15 15:13:59 +01:00
schizza 8e97fc7404 Different values displayed on station/cloud and HA - Total rainfall
Fixes #62

Fixes rainfall unit inconsistency

Updates daily rain sensor to use consistent measurement units and corrects device class and suggested unit.

Fixes #62

Adds database path constant and data migration function

Defines DATABASE_PATH constant for database file location
Introduces migrate_data function to update unit of measurement in long statistics.

Adds sensor migration feature

Introduces a migration step for sensor statistics
Updates schema and translations to support migration
Fixes data migration from mm/d to mm
2025-03-15 15:10:49 +01:00
Lukas Svoboda 908a60ade4
Merge pull request #61 from schizza/invalid_native_temp
Corrected temperature unit in outside_temp.
2025-03-11 17:40:28 +01:00
schizza 47143d9ac2 Corrected temperature unit in outside_temp.
Changes temperature unit from Celsius to Fahrenheit in the weather sensor descriptions.
2025-03-11 17:39:09 +01:00
Lukas Svoboda 41e2415e69
Update manifest.json 2025-03-10 08:45:32 +01:00
Lukas Svoboda 7ca4578c0f
Merge pull request #59 from schizza/wslink_sensors_add
Adds sensors for WSLink and improves data handling
2025-03-10 08:44:11 +01:00
schizza 37c3e2e77f Adds sensors for WSLink and improves data handling
Includes new sensor connection status constants and mappings
Updates WeatherSensor to handle null data values
Switches temperature unit to Celsius for WSLink sensors
Refines heat and chill index calculations with unit conversions
2025-03-10 08:42:17 +01:00
Lukas Svoboda cd5abc0db3
Merge pull request #52 from schizza/readme_update
readme_update
2025-02-21 19:06:26 +01:00
Lukas Svoboda 0c33ac02da
Merge branch 'main' into readme_update 2025-02-21 19:06:02 +01:00
schizza 3cd21e0143 readme_update 2025-02-21 18:05:15 +00:00
schizza 1dd51f4ed9 gitignore 2025-02-19 17:50:50 +01:00
Lukas Svoboda 7257587b2f
Update minor version 2025-02-18 19:43:36 +01:00
Lukas Svoboda c131eddf39
Merge pull request #51 from schizza/config_flow_fix
Improves configuration defaults and UI descriptions

Sets default values for optional fields in configuration flow
Enhances descriptions and titles in UI for better clarity
Updates translations
2025-02-18 19:42:20 +01:00
schizza a3dc4f9986 Improves configuration defaults and UI descriptions
Sets default values for optional fields in configuration flow
Enhances descriptions and titles in UI for better clarity
Updates translations
2025-02-18 19:40:46 +01:00
Lukas Svoboda 5395103c01
Merge pull request #49 from schizza/webook_switching
Refactoring webhook switching.
Added routing management to automaticly update coordinator to avoid restarting HA on configuration change or on new sensor discovery.
2025-02-15 18:23:06 +01:00
schizza 808fad1d97 webhook refactor,
Refactoring webhook switching.
Added routing management to automaticly update coordinator to avoid restarting HA on configuration change or on new sensor discovery.
2025-02-15 18:16:54 +01:00
Lukas Svoboda bbb6a23b6c
Merge pull request #45 from schizza/config_flow_depr_fix
Fix config flow
2025-02-13 17:33:16 +01:00
schizza d9cb2179d5 Fix config deprecation
Updated config flow to comply with new changes in HA
2025-02-13 17:32:07 +01:00
schizza 25bb26bb4d WSLink support in beta
Support for firmware 3.0 and WSLink connections.
As for now the 3.0 firmware sending data only on SSL connections.
2025-02-12 13:13:46 +01:00
Lukas Svoboda 3e27b889c4
Merge pull request #42 from Celer21/main
Update cs.json
2025-02-11 08:02:39 +01:00
Celer21 f295db69ff
Update cs.json
Minor translation update
2025-02-05 10:53:52 +01:00
Lukas Svoboda 4add593bf4
Merge pull request #40 from schizza/FW30Warning
Firmaware 3.0 not working
2025-01-26 10:40:13 +01:00
schizza 8d1995721b Firmaware 3.0 not working
README update.
2025-01-26 10:37:37 +01:00
Lukas Svoboda a6707b849b
Merge pull request #31 from schizza/async_fix
aiohttp fix
2024-12-14 14:36:13 +01:00
schizza 6a913cf447 aiohttp fix
Fixing blocking call from session.get to no longer throw warnings when sending data to Windy.
2024-12-14 14:34:11 +01:00
Lukas Svoboda 407bc40219
Merge pull request #30 from schizza/channel3_4
Bug fix
2024-07-15 23:24:27 -04:00
schizza 1f2c553d6a Bug fix 2024-07-15 22:23:27 -05:00
schizza c56ef9c2ce Minor changes 2024-07-15 22:22:16 -05:00
Lukas Svoboda 6c127342aa
Merge pull request #29 from schizza/channel3_4
Support for CH3 and CH4
2024-07-15 23:16:21 -04:00
schizza 66a9993dfc Support for CH3 and CH4 2024-07-15 22:14:20 -05:00
Lukas Svoboda 9b9fd16513
Merge pull request #27 from schizza/chill_index_fix
Chill index fix
2024-06-17 14:40:23 +02:00
schizza fb9830cc1e Chill index fix
Fix chill index formula as suggested by @facko79.
Chill index is computed only for temperatures  less then 10°C (50°F). Otherwise real temperature is returned.
2024-06-17 14:37:50 +02:00
Lukas Svoboda 615b384487
Merge pull request #24 from schizza/chill_index
Wind chill index
2024-05-09 14:18:48 +02:00
Lukas Svoboda 4548d413d5
Merge branch 'main' into chill_index 2024-05-09 14:18:33 +02:00
Lukas Svoboda 3229c40387
Merge pull request #23 from schizza/Heatindex
Add Heat Index
2024-05-09 14:18:12 +02:00
schizza e18762579b Wind chill index
* Add sensor for wind chill index
2024-05-09 14:17:01 +02:00
schizza 8b0cc2d25e Add Heat Index
* Heat index sensor
* Heat index computation is added
2024-05-09 13:26:01 +02:00
schizza b58ff1b2e5
Update firmware_bug.md 2024-05-03 21:18:53 +02:00
schizza b0e6348e6f Fix permissions in iptables script. 2024-05-03 18:19:21 +02:00
schizza 23b5608727 Moved automation to automations.yaml 2024-05-03 10:51:40 +02:00
schizza 8199b38aa0 Typo 2024-05-03 10:30:28 +02:00
schizza 050b326865 Fix path to automation script 2024-05-03 10:23:50 +02:00
schizza ea47c808b0
Merge pull request #22 from schizza/error-readme
Fix path discovery in HA
2024-05-03 10:21:04 +02:00
schizza acebf1b268 Fix permissions 2024-05-03 10:19:14 +02:00
schizza 0409955801 Final fix 2024-05-03 09:28:00 +02:00
schizza e84ec8f69e
Merge pull request #21 from schizza/error-readme
Fix path discovery in HA
2024-05-03 09:21:52 +02:00
schizza c92d2f53e5
Merge branch 'main' into error-readme 2024-05-03 09:21:41 +02:00
schizza b6edb63051 Fix path discovery in HA 2024-05-03 09:18:52 +02:00
schizza 782e04cd47
Update install_iptables.sh
Update to write right path to exec.sh
2024-05-02 20:26:58 +02:00
schizza 964ba623b2
Merge pull request #20 from schizza/schizza-patch-1
Update firmware_bug.md
2024-05-01 13:44:35 +02:00
schizza a691dc42dc
Update firmware_bug.md 2024-05-01 13:44:18 +02:00
schizza 0c0e9e145d
Update firmware_bug.md 2024-05-01 13:41:18 +02:00
schizza 0610cf7966
Merge pull request #19 from schizza/error-readme
Firmware bug README
2024-05-01 13:39:56 +02:00
schizza f49f77acd9 Firmware bug README 2024-05-01 11:37:09 +00:00
schizza 5eeda36f6d Add automation for HA 2024-04-30 17:42:59 +02:00
schizza 2271564048 chmod change a+rx 2024-04-30 17:24:03 +02:00
schizza edc63b83cb Minor tweaks 2024-04-30 17:17:54 +02:00
schizza 88a4f2af49
Merge pull request #18 from schizza/iptables-script
Iptables script
2024-04-30 17:12:38 +02:00
schizza 4f2e93e63e
Merge branch 'main' into iptables-script 2024-04-30 17:12:22 +02:00
schizza 6a9daa063d SSH, yaml config, scripts gen.
* Add SSH pair keys generation
* configuration.yaml auto add shell_command
* scripts to run shell_command properly
2024-04-30 17:06:08 +02:00
schizza 4f9ef266e0 Add script directory 2024-04-30 14:33:43 +02:00
schizza 6b4ce5f49f Typo 2024-04-30 14:29:07 +02:00
schizza cd60d224f4 Remove old files.
Remove iptables_redirect.sh if exists to download new one.
2024-04-30 14:29:07 +02:00
schizza d1b7986550 Minor changes. 2024-04-30 14:29:07 +02:00
schizza 3f1869e6a9 Fixing typos a minor changes 2024-04-30 14:29:07 +02:00
schizza be93b490e0 Minor tweaks. 2024-04-30 14:29:07 +02:00
schizza 5dc35ec7ad Link updates 2024-04-30 14:29:07 +02:00
schizza a916e342f7 Typo 2024-04-28 17:57:18 +02:00
schizza d0a0af17c8 Remove old files.
Remove iptables_redirect.sh if exists to download new one.
2024-04-28 17:40:57 +02:00
schizza 156cbc266b Minor changes. 2024-04-28 17:32:32 +02:00
schizza 5f847a3bb7 Fixing typos a minor changes 2024-04-28 17:30:54 +02:00
schizza c1a5afb510 Minor tweaks. 2024-04-28 17:00:42 +02:00
schizza dc263083c4 Link updates 2024-04-28 16:33:33 +02:00
schizza 2ee8cc5ed3
Merge pull request #16 from schizza/iptables-script
* added installation script

* iptables script translated to english and minor tweaks
2024-04-28 15:39:15 +02:00
schizza 69909e228c Installation script and tweaks in iptables script.
* added installation script

* iptables script translated to english and minor tweaks
2024-04-28 15:37:53 +02:00
schizza fa6caf4c22 Script fixed.
* Duplicity rule test
* color loging
2024-04-28 02:35:04 +02:00
schizza 6b3e83f4ca iptables script
In stations firmware 1.0 is bug for sending data to designated port.
This script will forward incoming connections on port 80 to 8123 for stations IP
2024-04-27 21:42:41 +02:00
schizza 34a3a968b0
Merge pull request #15 from MilanPala/cs-type-vetrtu
Typo in cs translation
2024-04-20 18:59:26 +02:00
Milan Pála 93193b2489
Typo in cs translation 2024-04-20 18:09:11 +02:00
schizza 54844f4755 Manifest alphabet order. 2024-04-19 17:06:58 +00:00
schizza d580c21055 Just cleanup.
Cleanup to satisfy HASSfest.
2024-04-19 15:46:13 +02:00
schizza 0b9780fb97
Merge pull request #14 from schizza/schizza-patch-1
Update validate.yml
2024-04-19 13:29:25 +02:00
schizza 6f29a50e5d
Update validate.yml 2024-04-19 13:29:08 +02:00
schizza df420369a2 HASSfest validation 2024-04-19 13:27:04 +02:00
schizza 640924ffcc HACS Validation
Validation workflow for HACS.
2024-04-19 12:58:07 +02:00
schizza 80e5f5a4ed
Merge pull request #13 from schizza/Bearing
New sensor fo Azimut now supported.

A new constant WIND_AZIMUT is added to allow creating a wind direction sensor that returns a text value from UnitOfDir instead of just a numeric degree value.

The wind direction sensor entity now optionally creates both a numeric wind direction sensor using WIND_DIR and a text-based azimuth sensor using WIND_AZIMUT. If the numeric one is enabled, the text one will also be added.

So in summary, these changes improve the usability of wind data by adding a more human-readable text version alongside the existing numeric version.
2024-04-18 21:14:53 +02:00
schizza ac7284770a New sensor fo Azimut now supported.
- A new constant `WIND_AZIMUT` is added to allow creating a wind direction sensor that returns a text value from `UnitOfDir` instead of just a numeric degree value.

- The wind direction sensor entity now optionally creates both a numeric wind direction sensor using `WIND_DIR` and a text-based azimuth sensor using `WIND_AZIMUT`. If the numeric one is enabled, the text one will also be added.

So in summary, these changes improve the usability of wind data by adding a more human-readable text version alongside the existing numeric version.
2024-04-18 20:54:57 +02:00
schizza 5fdd594bae
Merge pull request #12 from schizza/7-ha-stops-registering-new-data
7 HA stops registering new data after a while.
2024-04-17 13:15:16 +02:00
schizza 70bfd56268 Ability to add newly discovered sensors.
* Add discovered sensors to loading queue.
* Notify users that we have new sensors and we need to restart Home Assistant
2024-04-17 13:12:47 +02:00
schizza b62e6abccf Autoloading new sensors. 2024-04-07 11:05:01 +02:00
31 changed files with 2513 additions and 501 deletions

5
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,5 @@
# These are supported funding model platforms
github: schizza
ko_fi: schizza
buy_me_a_coffee: schizza

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Provide `Developer log` if applicable
**Provide information about your station:**
- Weather station type:
- firmware version:
- [ ] Using PWS protocol
- [ ] Using WSLink API
- [ ] Using WSLink proxy Add-on
**Additional context**
Add any other context about the problem here.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Is your feature request a new addition**
Describe what you want to achieve. How new feature should work.
**Additional context**
Add any other context or screenshots about the feature request here.

34
.github/ISSUE_TEMPLATE/issue.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Issue
about: A minor issue that does not significantly affect functionality.
title: "[ISSUE]"
labels: issue
assignees: ''
---
**Describe the issue**
A clear and concise description of what the issue is.
**To Reproduce**
Steps to reproduce the behavior if any:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Provide `Developer log` if applicable
**Provide information about your station:**
- Weather station type:
- firmware version:
- [ ] Using PWS protocol
- [ ] Using WSLink API
- [ ] Using WSLink proxy Add-on
**Additional context**
Add any other context about the problem here.

14
.github/workflows/hassfest.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Validate with hassfest
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- uses: home-assistant/actions/hassfest@master

18
.github/workflows/validate.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: HACS validate
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
validate-hacs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"

5
.gitignore vendored
View File

@ -160,3 +160,8 @@ cython_debug/
#.idea/
.DS_Store
HA
.devcontainer
.gitignore
.vscode

View File

@ -1,23 +1,41 @@
# Integrates your SWS 12500 weather station seamlessly into Home Assistant
# Integrates your Sencor SWS 12500 or 16600, 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 custom component replaces [old integration via Node-RED and proxy server](https://github.com/schizza/WeatherStation-SWS12500).*
_This custom component replaces [old integration via Node-RED and proxy server](https://github.com/schizza/WeatherStation-SWS12500)._
## Warning - WSLink APP (applies also for SWS 12500 with firmware >3.0)
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.
## Requirements
- [Sencor SWS 12500 Weather Station](https://www.sencor.cz/profesionalni-meteorologicka-stanice/sws-12500).
- 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.
- If you want to push data to Windy, you have to create an account at [Windy](https://stations.windy.com).
## List of supported stations
- [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)
- 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)
- Garni stations with WSLink support or custom server support.
## Installation
### 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)
### 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
For installation with HACS, you have to first add a [custom repository](https://hacs.xyz/docs/faq/custom_repositories/).
You will need to enter the URL of this repository when prompted: `https://github.com/schizza/SWS-12500-custom-component`.
After adding this repository to HACS:
- Go to HACS -> Integrations
- Search for the integration `Sencor SWS 12500 Weather station` and download the integration.
- Restart Home Assistant
@ -25,7 +43,7 @@ After adding this repository to HACS:
### Manual installation
For manual installation you must have an 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 [latest release here](https://github.com/schizza/SWS-12500-custom-component/releases/latest).
@ -35,13 +53,17 @@ For manual installation you must have an access to your Home Assistant's `/conf
## Configure your station in AP mode
> This configuration example is for Sencor SWS12500 with FW < 3.0
> For WSLink read [this notes.](#wslink-notes)
1. Hold the Wi-Fi button on the back of the station for 6 seconds until the AP will flash on the display.
2. Select your station from available APs on your computer.
3. Connect to the station's setup page: `http://192.168.1.1` from your browser.
4. In the third URL section fill in the address to your local Home Assistant installation.
5. Create new `ID` and `KEY`. You can use [online tool](https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx) to generate random keys. *(you will need them to configure integation to Home Assistatnt)*
5. Create new `ID` and `KEY`. You can use [online tool](https://randomkeygen.com) to generate random keys. _(you will need them to configure integration to Home Assistant)_
6. Save your configuration.
![station_setup](README/station_hint.png)
![station_setup](README/station_hint.png)
Once integration is added to Home Assistant, configuration dialog will ask you for `API_ID` and `API_KEY` as you set them in your station:
@ -52,7 +74,7 @@ API_KEY: PASSWORD in station's config
![config dialog](README/cfg_dialog.png)
If you chanage `API ID` or `API KEY` in the station, you have to reconfigure integration to accept data from your station.
If you change `API ID` or `API KEY` in the station, you have to reconfigure integration to accept data from your station.
- In `Settings` -> `Devices & services` find SWS12500 and click `Configure`.
- In dialog box choose `Basic - Configure credentials`
@ -65,14 +87,34 @@ As soon as the integration is added into Home Assistant it will listen for incom
- First of all you need to create account at [Windy stations](https://stations.windy.com).
- Once you have an account created, copy your Windy API Key.
![windy api key](README/windy_key.png)
![windy api key](README/windy_key.png)
- In `Settings` -> `Devices & services` find SWS12500 and click `Configure`.
- In dialog box choose `Windy configuration`.
![config dialog](README/cfg.png)
![config dialog](README/cfg.png)
- Fill in `Key` you were provided at `Windy stations`.
- Tick `Enable` checkbox.
![enable windy](README/windy_cfg.png)
![enable windy](README/windy_cfg.png)
- You are done.
## WSLink notes
While your station is using WSLink you have to have Home Assistant in SSL mode or behind SSL proxy server.
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.
### Configuration
- 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!
```plain
HomeAssistant is at 192.0.0.2:8123
WSLink proxy addon listening on port 4443
you will set URL in station to: 192.0.0.2:4443
```
- Your station will be sending data to this SSL proxy and addon will handle the rest.
_Most of the stations does not care about self-signed certificates on the server side._

BIN
README/addon_ssh_config.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
README/key.mp4 Normal file

Binary file not shown.

BIN
README/script_run.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

View File

@ -1,4 +1,5 @@
"""The Sencor SWS 12500 Weather Station integration."""
import logging
import aiohttp
@ -8,15 +9,34 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import InvalidStateError, PlatformNotReady
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import API_ID, API_KEY, DEFAULT_URL, DEV_DBG, DOMAIN, WINDY_ENABLED
from .utils import anonymize, check_disabled, remap_items
from .const import (
API_ID,
API_KEY,
DEFAULT_URL,
DEV_DBG,
DOMAIN,
SENSORS_TO_LOAD,
WINDY_ENABLED,
WSLINK,
WSLINK_URL,
)
from .routes import Routes, unregistred
from .utils import (
anonymize,
check_disabled,
loaded_sensors,
remap_items,
remap_wslink_items,
translated_notification,
translations,
update_options,
)
from .windy_func import WindyPush
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.SENSOR]
class IncorrectDataError(InvalidStateError):
@ -35,15 +55,25 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
async def recieved_data(self, webdata):
"""Handle incoming data query."""
_wslink = self.config_entry.options.get(WSLINK)
data = webdata.query
response = None
if "ID" not in data or "PASSWORD" not in data:
if not _wslink and ("ID" not in data or "PASSWORD" not in data):
_LOGGER.error("Invalid request. No security data provided!")
raise HTTPUnauthorized
id_data = data["ID"]
key_data = data["PASSWORD"]
if _wslink and ("wsid" not in data or "wspw" not in data):
_LOGGER.error("Invalid request. No security data provided!")
raise HTTPUnauthorized
if _wslink:
id_data = data["wsid"]
key_data = data["wspw"]
else:
id_data = data["ID"]
key_data = data["PASSWORD"]
_id = self.config_entry.options.get(API_ID)
_key = self.config_entry.options.get(API_KEY)
@ -55,61 +85,117 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
if self.config_entry.options.get(WINDY_ENABLED):
response = await self.windy.push_data_to_windy(data)
remaped_items = remap_items(data)
remaped_items = (
remap_wslink_items(data)
if self.config_entry.options.get(WSLINK)
else remap_items(data)
)
await check_disabled(self.hass, remaped_items, self.config_entry.options.get(DEV_DBG))
if sensors := check_disabled(self.hass, remaped_items, self.config):
translate_sensors = [
await translations(
self.hass, DOMAIN, f"sensor.{t_key}", key="name", category="entity"
)
for t_key in sensors
if await translations(
self.hass, DOMAIN, f"sensor.{t_key}", key="name", category="entity"
)
is not None
]
human_readable = "\n".join(translate_sensors)
await translated_notification(
self.hass,
DOMAIN,
"added",
{"added_sensors": f"{human_readable}\n"},
)
if _loaded_sensors := loaded_sensors(self.config_entry):
sensors.extend(_loaded_sensors)
await update_options(self.hass, self.config_entry, SENSORS_TO_LOAD, sensors)
# await self.hass.config_entries.async_reload(self.config.entry_id)
self.async_set_updated_data(remaped_items)
if self.config_entry.options.get(DEV_DBG):
_LOGGER.info("Dev log: %s", anonymize(data))
response = response if response else "OK"
return aiohttp.web.Response(body=f"{response}", status=200)
response = response or "OK"
return aiohttp.web.Response(body=f"{response or 'OK'}", status=200)
def register_path(
hass: HomeAssistant, url_path: str, coordinator: WeatherDataUpdateCoordinator
hass: HomeAssistant,
url_path: str,
coordinator: WeatherDataUpdateCoordinator,
config: ConfigEntry,
):
"""Register path to handle incoming data."""
try:
route = hass.http.app.router.add_route(
"GET", url_path, coordinator.recieved_data
hass_data = hass.data.setdefault(DOMAIN, {})
debug = config.options.get(DEV_DBG)
_wslink = config.options.get(WSLINK)
routes: Routes = hass_data.get("routes") if "routes" in hass_data else None
if routes is None:
routes = Routes()
_LOGGER.info("Routes not found, creating new routes")
if debug:
_LOGGER.debug("Enabled route is: %s, WSLink is %s", url_path, _wslink)
try:
default_route = hass.http.app.router.add_get(
DEFAULT_URL,
coordinator.recieved_data if not _wslink else unregistred,
name="weather_default_url",
)
if debug:
_LOGGER.debug("Default route: %s", default_route)
wslink_route = hass.http.app.router.add_get(
WSLINK_URL,
coordinator.recieved_data if _wslink else unregistred,
name="weather_wslink_url",
)
if debug:
_LOGGER.debug("WSLink route: %s", wslink_route)
routes.add_route(
DEFAULT_URL,
default_route,
coordinator.recieved_data if not _wslink else unregistred,
not _wslink,
)
routes.add_route(
WSLINK_URL, wslink_route, coordinator.recieved_data, _wslink
)
hass_data["routes"] = routes
except RuntimeError as Ex: # pylint: disable=(broad-except)
if (
"Added route will never be executed, method GET is already registered"
in Ex.args
):
_LOGGER.info("Handler to URL (%s) already registred", url_path)
return False
_LOGGER.error("Unable to register URL handler! (%s)", Ex.args)
return False
_LOGGER.info(
"Registered path to handle weather data: %s",
routes.get_enabled(), # pylint: disable=used-before-assignment
)
except RuntimeError as Ex: # pylint: disable=(broad-except)
if "Added route will never be executed, method GET is already registered" in Ex.args:
_LOGGER.info("Handler to URL (%s) already registred", url_path)
return True
_LOGGER.error("Unable to register URL handler! (%s)", Ex.args)
return False
if _wslink:
routes.switch_route(coordinator.recieved_data, WSLINK_URL)
else:
routes.switch_route(coordinator.recieved_data, DEFAULT_URL)
_LOGGER.info(
"Registered path to handle weather data: %s",
route.get_info(), # pylint: disable=used-before-assignment
)
return True
def unregister_path(hass: HomeAssistant):
"""Unregister path to handle incoming data."""
_LOGGER.error(
"Unable to delete webhook from API! Restart HA before adding integration!"
)
class Weather(WeatherDataUpdateCoordinator):
"""Weather class."""
def __init__(self, hass: HomeAssistant, config) -> None:
"""Init class."""
self.hass = hass
super().__init__(hass, config)
async def setup_update_listener(self, hass: HomeAssistant, entry: ConfigEntry):
"""Update setup listener."""
await hass.config_entries.async_reload(entry.entry_id)
_LOGGER.info("Settings updated")
return routes
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -117,38 +203,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = WeatherDataUpdateCoordinator(hass, entry)
hass.data.setdefault(DOMAIN, {})
hass_data = hass.data.setdefault(DOMAIN, {})
hass_data[entry.entry_id] = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
_wslink = entry.options.get(WSLINK)
debug = entry.options.get(DEV_DBG)
weather = Weather(hass, entry)
if debug:
_LOGGER.debug("WS Link is %s", "enbled" if _wslink else "disabled")
if not register_path(hass, DEFAULT_URL, coordinator):
route = register_path(
hass, DEFAULT_URL if not _wslink else WSLINK_URL, coordinator, entry
)
if not route:
_LOGGER.error("Fatal: path not registered!")
raise PlatformNotReady
hass_data["route"] = route
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(weather.setup_update_listener))
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Update setup listener."""
await hass.config_entries.async_reload(entry.entry_id)
_LOGGER.info("Settings updated")
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if _ok:
hass.data[DOMAIN].pop(entry.entry_id)
unregister_path(hass)
return _ok
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component.
This component can only be configured through the Integrations UI.
"""
hass.data.setdefault(DOMAIN, {})
return True

View File

@ -1,11 +1,15 @@
"""Config flow for Sencor SWS 12500 Weather Station integration."""
import logging
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlow, OptionsFlow
from homeassistant.const import UnitOfPrecipitationDepth, UnitOfVolumetricFlux
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.entity_registry as er
from .const import (
API_ID,
@ -13,10 +17,18 @@ from .const import (
DEV_DBG,
DOMAIN,
INVALID_CREDENTIALS,
MIG_FROM,
MIG_TO,
SENSOR_TO_MIGRATE,
SENSORS_TO_LOAD,
WINDY_API_KEY,
WINDY_ENABLED,
WINDY_LOGGER_ENABLED,
WSLINK,
)
from .utils import long_term_units_in_statistics_meta, migrate_data
_LOGGER = logging.getLogger(__name__)
class CannotConnect(HomeAssistantError):
@ -27,50 +39,115 @@ class InvalidAuth(HomeAssistantError):
"""Invalid auth exception."""
class ConfigOptionsFlowHandler(config_entries.OptionsFlow):
class ConfigOptionsFlowHandler(OptionsFlow):
"""Handle WeatherStation ConfigFlow."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
def __init__(self) -> None:
"""Initialize flow."""
self.config_entry = config_entry
super().__init__()
self.user_data: dict[str, str] = {
self.windy_data: dict[str, Any] = {}
self.windy_data_schema = {}
self.user_data: dict[str, Any] = {}
self.user_data_schema = {}
self.sensors: dict[str, Any] = {}
self.migrate_schema = {}
self.migrate_sensor_select = {}
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):
"""Get entry data."""
self.user_data: dict[str, Any] = {
API_ID: self.config_entry.options.get(API_ID),
API_KEY: self.config_entry.options.get(API_KEY),
DEV_DBG: self.config_entry.options.get(DEV_DBG),
WSLINK: self.config_entry.options.get(WSLINK, False),
DEV_DBG: self.config_entry.options.get(DEV_DBG, False),
}
self.user_data_schema = {
vol.Required(API_ID, default=self.user_data.get(API_ID, "")): str,
vol.Required(API_KEY, default=self.user_data.get(API_KEY, "")): str,
vol.Optional(WSLINK, default=self.user_data.get(WSLINK, False)): bool,
vol.Optional(DEV_DBG, default=self.user_data.get(DEV_DBG, False)): bool,
}
self.sensors: dict[str, Any] = {
SENSORS_TO_LOAD: self.config_entry.options.get(SENSORS_TO_LOAD)
if isinstance(self.config_entry.options.get(SENSORS_TO_LOAD), list)
else []
}
self.windy_data: dict[str, Any] = {
WINDY_API_KEY: self.config_entry.options.get(WINDY_API_KEY),
WINDY_ENABLED: self.config_entry.options.get(WINDY_ENABLED) if isinstance(self.config_entry.options.get(WINDY_ENABLED), bool) else False,
WINDY_LOGGER_ENABLED: self.config_entry.options.get(WINDY_LOGGER_ENABLED) if isinstance(self.config_entry.options.get(WINDY_LOGGER_ENABLED), bool) else False,
}
self.user_data_schema = {
vol.Required(API_ID, default=self.user_data[API_ID] or ""): str,
vol.Required(API_KEY, default=self.user_data[API_KEY] or ""): str,
vol.Optional(DEV_DBG, default=self.user_data[DEV_DBG]): bool,
WINDY_ENABLED: self.config_entry.options.get(WINDY_ENABLED, False),
WINDY_LOGGER_ENABLED: self.config_entry.options.get(
WINDY_LOGGER_ENABLED, False
),
}
self.windy_data_schema = {
vol.Optional(
WINDY_API_KEY, default=self.windy_data[WINDY_API_KEY] or ""
WINDY_API_KEY, default=self.windy_data.get(WINDY_API_KEY, "")
): str,
vol.Optional(WINDY_ENABLED, default=self.windy_data[WINDY_ENABLED]): bool,
vol.Optional(WINDY_ENABLED, default=self.windy_data[WINDY_ENABLED]): bool
or False,
vol.Optional(
WINDY_LOGGER_ENABLED,
default=self.windy_data[WINDY_LOGGER_ENABLED],
): bool,
): bool or False,
}
self.migrate_sensor_select = {
vol.Required(SENSOR_TO_MIGRATE): vol.In(
await self.load_sensors_to_migrate() or {}
),
}
self.migrate_unit_selection = {
vol.Required(MIG_FROM): vol.In(self.unit_values),
vol.Required(MIG_TO): vol.In(self.unit_values),
vol.Optional("trigger_action", default=False): bool,
}
# "mm/d", "mm/h", "mm", "in/d", "in/h", "in"
async def load_sensors_to_migrate(self) -> dict[str, Any]:
"""Load sensors to migrate."""
sensor_statistics = await long_term_units_in_statistics_meta(self.hass)
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):
"""Manage the options - show menu first."""
return self.async_show_menu(step_id="init", menu_options=["basic", "windy"])
return self.async_show_menu(
step_id="init", menu_options=["basic", "windy", "migration"]
)
async def async_step_basic(self, user_input=None):
"""Manage basic options - credentials."""
errors = {}
await self._get_entry_data()
if user_input is None:
return self.async_show_form(
step_id="basic",
@ -85,17 +162,12 @@ class ConfigOptionsFlowHandler(config_entries.OptionsFlow):
elif user_input[API_KEY] == user_input[API_ID]:
errors["base"] = "valid_credentials_match"
else:
# retain Windy options
data: dict = {}
data[WINDY_API_KEY] = self.config_entry.options.get(WINDY_API_KEY)
data[WINDY_ENABLED] = self.config_entry.options.get(WINDY_ENABLED)
data[WINDY_LOGGER_ENABLED] = self.config_entry.options.get(
WINDY_LOGGER_ENABLED
)
# retain windy data
user_input.update(self.windy_data)
# retain sensors
user_input.update(self.sensors)
return self.async_create_entry(title=DOMAIN, data=user_input)
self.user_data = user_input
@ -111,6 +183,8 @@ class ConfigOptionsFlowHandler(config_entries.OptionsFlow):
"""Manage windy options."""
errors = {}
await self._get_entry_data()
if user_input is None:
return self.async_show_form(
step_id="windy",
@ -122,26 +196,171 @@ class ConfigOptionsFlowHandler(config_entries.OptionsFlow):
errors[WINDY_API_KEY] = "windy_key_required"
return self.async_show_form(
step_id="windy",
data_schema=self.windy_data_schema,
description_placeholders={
WINDY_ENABLED: True,
WINDY_LOGGER_ENABLED: user_input[WINDY_LOGGER_ENABLED],
},
data_schema=vol.Schema(self.windy_data_schema),
errors=errors,
)
# 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)
async def async_step_migration(self, user_input=None):
"""Migrate sensors."""
errors = {}
data_schema = vol.Schema(self.migrate_sensor_select)
data_schema.schema.update()
await self._get_entry_data()
if user_input is None:
return self.async_show_form(
step_id="migration",
data_schema=vol.Schema(self.migrate_sensor_select),
errors=errors,
description_placeholders={
"migration_status": "-",
"migration_count": "-",
},
)
self.selected_sensor = user_input.get(SENSOR_TO_MIGRATE)
return await self.async_step_migration_units()
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(
step_id="migration_units",
data_schema=data_schema,
errors={},
description_placeholders={
"migration_sensor": sensor_entry.original_name,
"migration_stats": sensor_stats.get(self.selected_sensor),
},
)
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)
# retain senors
user_input.update(self.sensors)
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)
# 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)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class ConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Sencor SWS 12500 Weather Station."""
data_schema = {
vol.Required(API_ID): str,
vol.Required(API_KEY): str,
vol.Optional(WSLINK): bool,
vol.Optional(DEV_DBG): bool,
}
@ -167,7 +386,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
elif user_input[API_KEY] == user_input[API_ID]:
errors["base"] = "valid_credentials_match"
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(
step_id="user",
@ -177,8 +398,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> ConfigOptionsFlowHandler:
def async_get_options_flow(config_entry) -> ConfigOptionsFlowHandler:
"""Get the options flow for this handler."""
return ConfigOptionsFlowHandler(config_entry)
return ConfigOptionsFlowHandler()

View File

@ -1,17 +1,24 @@
"""Constants."""
from enum import StrEnum
from typing import Final
DOMAIN = "sws12500"
DEFAULT_URL = "/weatherstation/updateweatherstation.php"
WSLINK_URL = "/data/upload.php"
WINDY_URL = "https://stations.windy.com/pws/update/"
DATABASE_PATH = "/config/home-assistant_v2.db"
ICON = "mdi:weather"
API_KEY = "API_KEY"
API_ID = "API_ID"
SENSORS_TO_LOAD: Final = "sensors_to_load"
SENSOR_TO_MIGRATE: Final = "sensor_to_migrate"
DEV_DBG: Final = "dev_debug_checkbox"
WSLINK: Final = "wslink"
WINDY_API_KEY = "WINDY_API_KEY"
WINDY_ENABLED: Final = "windy_enabled_checkbox"
@ -50,14 +57,21 @@ PURGE_DATA: Final = [
"dailyrainin",
]
BARO_PRESSURE: Final = "baro_pressure"
OUTSIDE_TEMP: Final = "outside_temp"
DEW_POINT: Final = "dew_point"
OUTSIDE_HUMIDITY: Final = "outside_humidity"
OUTSIDE_CONNECTION: Final = "outside_connection"
WIND_SPEED: Final = "wind_speed"
WIND_GUST: Final = "wind_gust"
WIND_DIR: Final = "wind_dir"
WIND_AZIMUT: Final = "wind_azimut"
RAIN: Final = "rain"
HOURLY_RAIN: Final = "hourly_rain"
WEEKLY_RAIN: Final = "weekly_rain"
MONTHLY_RAIN: Final = "monthly_rain"
YEARLY_RAIN: Final = "yearly_rain"
DAILY_RAIN: Final = "daily_rain"
SOLAR_RADIATION: Final = "solar_radiation"
INDOOR_TEMP: Final = "indoor_temp"
@ -65,6 +79,15 @@ INDOOR_HUMIDITY: Final = "indoor_humidity"
UV: Final = "uv"
CH2_TEMP: Final = "ch2_temp"
CH2_HUMIDITY: Final = "ch2_humidity"
CH2_CONNECTION: Final = "ch2_connection"
CH3_TEMP: Final = "ch3_temp"
CH3_HUMIDITY: Final = "ch3_humidity"
CH3_CONNECTION: Final = "ch3_connection"
CH4_TEMP: Final = "ch4_temp"
CH4_HUMIDITY: Final = "ch4_humidity"
CH4_CONNECTION: Final = "ch4_connection"
HEAT_INDEX: Final = "heat_index"
CHILL_INDEX: Final = "chill_index"
REMAP_ITEMS: dict = {
@ -83,9 +106,92 @@ REMAP_ITEMS: dict = {
"UV": UV,
"soiltempf": CH2_TEMP,
"soilmoisture": CH2_HUMIDITY,
"soiltemp2f": CH3_TEMP,
"soilmoisture2": CH3_HUMIDITY,
"soiltemp3f": CH4_TEMP,
"soilmoisture3": CH4_HUMIDITY,
}
REMAP_WSLINK_ITEMS: dict = {
"intem": INDOOR_TEMP,
"inhum": INDOOR_HUMIDITY,
"t1tem": OUTSIDE_TEMP,
"t1hum": OUTSIDE_HUMIDITY,
"t1dew": DEW_POINT,
"t1wdir": WIND_DIR,
"t1ws": WIND_SPEED,
"t1wgust": WIND_GUST,
"t1rainra": RAIN,
"t1raindy": DAILY_RAIN,
"t1solrad": SOLAR_RADIATION,
"rbar": BARO_PRESSURE,
"t1uvi": UV,
"t234c1tem": CH2_TEMP,
"t234c1hum": CH2_HUMIDITY,
"t1cn": OUTSIDE_CONNECTION,
"t234c1cn": CH2_CONNECTION,
"t234c2cn": CH3_CONNECTION,
"t1chill": CHILL_INDEX,
"t1heat": HEAT_INDEX,
"t1rainhr": HOURLY_RAIN,
"t1rainwy": WEEKLY_RAIN,
"t1rainmth": MONTHLY_RAIN,
"t1rainyr": YEARLY_RAIN,
}
# 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 = [
CH2_TEMP,
CH2_HUMIDITY
CH2_HUMIDITY,
CH3_TEMP,
CH3_HUMIDITY,
CH4_TEMP,
CH4_HUMIDITY,
]
class UnitOfDir(StrEnum):
"""Wind direrction azimut."""
NNE = "nne"
NE = "ne"
ENE = "ene"
E = "e"
ESE = "ese"
SE = "se"
SSE = "sse"
S = "s"
SSW = "ssw"
SW = "sw"
WSW = "wsw"
W = "w"
WNW = "wnw"
NW = "nw"
NNW = "nnw"
N = "n"
AZIMUT: list[UnitOfDir] = [
UnitOfDir.NNE,
UnitOfDir.NE,
UnitOfDir.ENE,
UnitOfDir.E,
UnitOfDir.ESE,
UnitOfDir.SE,
UnitOfDir.SSE,
UnitOfDir.S,
UnitOfDir.SSW,
UnitOfDir.SW,
UnitOfDir.WSW,
UnitOfDir.W,
UnitOfDir.WNW,
UnitOfDir.NW,
UnitOfDir.NNW,
UnitOfDir.N,
]

View File

@ -3,13 +3,13 @@
"name": "Sencor SWS 12500 Weather Station",
"codeowners": ["@schizza"],
"config_flow": true,
"issue_tracker": "https://github.com/schizza/SWS-12500-custom-component/issues",
"dependencies": ["http"],
"documentation": "https://github.com/schizza/SWS-12500-custom-component",
"homekit": {},
"iot_class": "local_push",
"issue_tracker": "https://github.com/schizza/SWS-12500-custom-component/issues",
"requirements": [],
"ssdp": [],
"version": "0.1.2",
"version": "1.6.2",
"zeroconf": []
}

View File

@ -1,46 +0,0 @@
@property
def translation_key(self):
"""Return translation key."""
return self.entity_description.translation_key
@property
def device_class(self):
"""Return device class."""
return self.entity_description.device_class
@property
def name(self) -> str:
"""Return the name of the switch."""
return str(self.entity_description.name)
@property
def unique_id(self) -> str:
"""Return a unique, Home Assistant friendly identifier for this entity."""
return self.entity_description.key
@property
def native_value(self):
"""Return value of entity."""
return self._state
@property
def icon(self) -> str:
"""Return icon of entity."""
return str(self.entity_description.icon)
@property
def native_unit_of_measurement(self) -> str:
"""Return unit of measurement."""
return str(self.entity_description.native_unit_of_measurement)
@property
def state_class(self) -> str:
"""Return stateClass."""
return str(self.entity_description.state_class)
@property
def suggested_unit_of_measurement(self) -> str:
"""Return sugestet_unit_of_measurement."""
return str(self.entity_description.suggested_unit_of_measurement)

View File

@ -0,0 +1,76 @@
"""Store routes info."""
from dataclasses import dataclass
from logging import getLogger
from aiohttp.web import AbstractRoute, Response
_LOGGER = getLogger(__name__)
@dataclass
class Route:
"""Store route info."""
url_path: str
route: AbstractRoute
handler: callable
enabled: bool = False
def __str__(self):
"""Return string representation."""
return f"{self.url_path} -> {self.handler}"
class Routes:
"""Store routes info."""
def __init__(self) -> None:
"""Initialize routes."""
self.routes = {}
def switch_route(self, coordinator: callable, url_path: str):
"""Switch route."""
for url, route in self.routes.items():
if url == url_path:
_LOGGER.info("New coordinator to route: %s", route.url_path)
route.enabled = True
route.handler = coordinator
route.route._handler = coordinator # noqa: SLF001
else:
route.enabled = False
route.handler = unregistred
route.route._handler = unregistred # noqa: SLF001
def add_route(
self,
url_path: str,
route: AbstractRoute,
handler: callable,
enabled: bool = False,
):
"""Add route."""
self.routes[url_path] = Route(url_path, route, handler, enabled)
def get_route(self, url_path: str) -> Route:
"""Get route."""
return self.routes.get(url_path)
def get_enabled(self) -> str:
"""Get enabled routes."""
enabled_routes = [
route.url_path for route in self.routes.values() if route.enabled
]
return "".join(enabled_routes) if enabled_routes else "None"
def __str__(self):
"""Return string representation."""
return "\n".join([str(route) for route in self.routes.values()])
async def unregistred(*args, **kwargs):
"""Unregister path to handle incoming data."""
_LOGGER.error("Recieved data to unregistred webhook. Check your settings")
return Response(body=f"{'Unregistred webhook.'}", status=404)

View File

@ -1,212 +1,36 @@
"""Sensors definition for SWS12500."""
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any, cast
from homeassistant.components.sensor import (
RestoreSensor,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
import logging
from homeassistant.components.sensor import RestoreSensor, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DEGREE,
PERCENTAGE,
UV_INDEX,
UnitOfIrradiance,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolumetricFlux,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo, generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import WeatherDataUpdateCoordinator
from .const import (
BARO_PRESSURE,
CH2_HUMIDITY,
CH2_TEMP,
DAILY_RAIN,
DEW_POINT,
CHILL_INDEX,
DOMAIN,
INDOOR_HUMIDITY,
INDOOR_TEMP,
HEAT_INDEX,
OUTSIDE_HUMIDITY,
OUTSIDE_TEMP,
RAIN,
SOLAR_RADIATION,
UV,
SENSORS_TO_LOAD,
WIND_AZIMUT,
WIND_DIR,
WIND_GUST,
WIND_SPEED,
WSLINK,
)
from .sensors_common import WeatherSensorEntityDescription
from .sensors_weather import SENSOR_TYPES_WEATHER_API
from .sensors_wslink import SENSOR_TYPES_WSLINK
from .utils import chill_index, heat_index
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class WeatherSensorEntityDescription(SensorEntityDescription):
"""Describe Weather Sensor entities."""
value_fn: Callable[[Any], int | float | str | None]
SENSOR_TYPES: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription(
key=INDOOR_TEMP,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE,
translation_key=INDOOR_TEMP,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
key=INDOOR_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.HUMIDITY,
translation_key=INDOOR_HUMIDITY,
value_fn=lambda data: cast(int, data),
),
WeatherSensorEntityDescription(
key=OUTSIDE_TEMP,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE,
translation_key=OUTSIDE_TEMP,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
key=OUTSIDE_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.HUMIDITY,
translation_key=OUTSIDE_HUMIDITY,
value_fn=lambda data: cast(int, data),
),
WeatherSensorEntityDescription(
key=DEW_POINT,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer-lines",
device_class=SensorDeviceClass.TEMPERATURE,
translation_key=DEW_POINT,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
key=BARO_PRESSURE,
native_unit_of_measurement=UnitOfPressure.INHG,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer-lines",
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
suggested_unit_of_measurement=UnitOfPressure.HPA,
translation_key=BARO_PRESSURE,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
key=WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
icon="mdi:weather-windy",
translation_key=WIND_SPEED,
value_fn=lambda data: cast(int, data),
),
WeatherSensorEntityDescription(
key=WIND_GUST,
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
icon="mdi:windsock",
translation_key=WIND_GUST,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
key=WIND_DIR,
native_unit_of_measurement=DEGREE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=None,
icon="mdi:sign-direction",
translation_key=WIND_DIR,
value_fn=lambda data: cast(int, data),
),
WeatherSensorEntityDescription(
key=RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=RAIN,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
key=DAILY_RAIN,
native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_DAY,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=DAILY_RAIN,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
key=SOLAR_RADIATION,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.IRRADIANCE,
icon="mdi:weather-sunny",
translation_key=SOLAR_RADIATION,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
key=UV,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UV_INDEX,
icon="mdi:sunglasses",
translation_key=UV,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
key=CH2_TEMP,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny",
translation_key=CH2_TEMP,
entity_registry_visible_default=False,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
key=CH2_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny",
translation_key=CH2_HUMIDITY,
entity_registry_visible_default=False,
value_fn=lambda data: cast(int, data),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@ -215,10 +39,28 @@ async def async_setup_entry(
"""Set up Weather Station sensors."""
coordinator: WeatherDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
sensors = []
for description in SENSOR_TYPES:
sensors.append(WeatherSensor(hass, description, coordinator))
async_add_entities(sensors)
sensors_to_load: list = []
sensors: list = []
_wslink = config_entry.data.get(WSLINK)
SENSOR_TYPES = SENSOR_TYPES_WSLINK if _wslink else SENSOR_TYPES_WEATHER_API
# Check if we have some sensors to load.
if sensors_to_load := config_entry.options.get(SENSORS_TO_LOAD):
if WIND_DIR in sensors_to_load:
sensors_to_load.append(WIND_AZIMUT)
if (OUTSIDE_HUMIDITY in sensors_to_load) and (OUTSIDE_TEMP in sensors_to_load):
sensors_to_load.append(HEAT_INDEX)
if (WIND_SPEED in sensors_to_load) and (OUTSIDE_TEMP in sensors_to_load):
sensors_to_load.append(CHILL_INDEX)
sensors = [
WeatherSensor(hass, description, coordinator)
for description in SENSOR_TYPES
if description.key in sensors_to_load
]
async_add_entities(sensors)
class WeatherSensor(
@ -226,7 +68,6 @@ class WeatherSensor(
):
"""Implementation of Weather Sensor entity."""
entity_description: WeatherSensorEntityDescription
_attr_has_entity_name = True
_attr_should_poll = False
@ -245,36 +86,56 @@ class WeatherSensor(
self._data = None
async def async_added_to_hass(self) -> None:
"""Handle disabled entities that has previous data."""
"""Handle listeners to reloaded sensors."""
await super().async_added_to_hass()
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
if not self.entity_registry_visible_default:
self.entity_registry_visible_default = True
# 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
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._data = self.coordinator.data.get(self.entity_description.key)
super()._handle_coordinator_update()
self.async_write_ha_state()
@property
def native_value(self) -> str | int | float | None:
"""Return value of entity."""
return self.entity_description.value_fn(self._data)
_wslink = self.coordinator.config.options.get(WSLINK)
if self.coordinator.data and (WIND_AZIMUT in self.entity_description.key):
return self.entity_description.value_fn(self.coordinator.data.get(WIND_DIR))
if (
self.coordinator.data
and (HEAT_INDEX in self.entity_description.key)
and not _wslink
):
return self.entity_description.value_fn(heat_index(self.coordinator.data))
if (
self.coordinator.data
and (CHILL_INDEX in self.entity_description.key)
and not _wslink
):
return self.entity_description.value_fn(chill_index(self.coordinator.data))
return None if self._data == "" else self.entity_description.value_fn(self._data)
@property
def state_class(self) -> str:
"""Return stateClass."""
return str(self.entity_description.state_class)
def suggested_entity_id(self) -> str:
"""Return name."""
return generate_entity_id("sensor.{}", self.entity_description.key)
@property
def device_info(self) -> DeviceInfo:

View File

@ -0,0 +1,14 @@
"""Common classes for sensors."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.sensor import SensorEntityDescription
@dataclass(frozen=True, kw_only=True)
class WeatherSensorEntityDescription(SensorEntityDescription):
"""Describe Weather Sensor entities."""
value_fn: Callable[[Any], int | float | str | None]

View File

@ -0,0 +1,258 @@
"""Sensor entities for the SWS12500 integration for old endpoint."""
from typing import cast
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import (
DEGREE,
PERCENTAGE,
UV_INDEX,
UnitOfIrradiance,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolumetricFlux,
)
from .const import (
BARO_PRESSURE,
CH2_HUMIDITY,
CH2_TEMP,
CH3_HUMIDITY,
CH3_TEMP,
CH4_HUMIDITY,
CH4_TEMP,
CHILL_INDEX,
DAILY_RAIN,
DEW_POINT,
HEAT_INDEX,
INDOOR_HUMIDITY,
INDOOR_TEMP,
OUTSIDE_HUMIDITY,
OUTSIDE_TEMP,
RAIN,
SOLAR_RADIATION,
UV,
WIND_AZIMUT,
WIND_DIR,
WIND_GUST,
WIND_SPEED,
UnitOfDir,
)
from .sensors_common import WeatherSensorEntityDescription
from .utils import wind_dir_to_text
SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription(
key=INDOOR_TEMP,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE,
translation_key=INDOOR_TEMP,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=INDOOR_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.HUMIDITY,
translation_key=INDOOR_HUMIDITY,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=OUTSIDE_TEMP,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE,
translation_key=OUTSIDE_TEMP,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=OUTSIDE_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.HUMIDITY,
translation_key=OUTSIDE_HUMIDITY,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=DEW_POINT,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer-lines",
device_class=SensorDeviceClass.TEMPERATURE,
translation_key=DEW_POINT,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=BARO_PRESSURE,
native_unit_of_measurement=UnitOfPressure.INHG,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer-lines",
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
suggested_unit_of_measurement=UnitOfPressure.HPA,
translation_key=BARO_PRESSURE,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
icon="mdi:weather-windy",
translation_key=WIND_SPEED,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=WIND_GUST,
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
icon="mdi:windsock",
translation_key=WIND_GUST,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=WIND_DIR,
native_unit_of_measurement=DEGREE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=None,
icon="mdi:sign-direction",
translation_key=WIND_DIR,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=WIND_AZIMUT,
icon="mdi:sign-direction",
value_fn=lambda data: cast("str", wind_dir_to_text(data)),
device_class=SensorDeviceClass.ENUM,
options=list(UnitOfDir),
translation_key=WIND_AZIMUT,
),
WeatherSensorEntityDescription(
key=RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=DAILY_RAIN,
native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_DAY,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=DAILY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=SOLAR_RADIATION,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.IRRADIANCE,
icon="mdi:weather-sunny",
translation_key=SOLAR_RADIATION,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=UV,
name=UV,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UV_INDEX,
icon="mdi:sunglasses",
translation_key=UV,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=CH2_TEMP,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny",
translation_key=CH2_TEMP,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=CH2_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny",
translation_key=CH2_HUMIDITY,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=CH3_TEMP,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny",
translation_key=CH3_TEMP,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=CH3_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny",
translation_key=CH3_HUMIDITY,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=CH4_TEMP,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny",
translation_key=CH4_TEMP,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=CH4_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny",
translation_key=CH4_HUMIDITY,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=HEAT_INDEX,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
icon="mdi:weather-sunny",
translation_key=HEAT_INDEX,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=CHILL_INDEX,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
icon="mdi:weather-sunny",
translation_key=CHILL_INDEX,
value_fn=lambda data: cast("int", data),
),
)

View File

@ -0,0 +1,306 @@
"""Sensor entities for the SWS12500 integration for old endpoint."""
from typing import cast
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import (
DEGREE,
PERCENTAGE,
UV_INDEX,
UnitOfIrradiance,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolumetricFlux,
)
from .const import (
BARO_PRESSURE,
CH2_HUMIDITY,
CH2_TEMP,
CH3_HUMIDITY,
CH3_TEMP,
CH4_HUMIDITY,
CH4_TEMP,
CHILL_INDEX,
DAILY_RAIN,
DEW_POINT,
HEAT_INDEX,
INDOOR_HUMIDITY,
INDOOR_TEMP,
OUTSIDE_HUMIDITY,
OUTSIDE_TEMP,
RAIN,
SOLAR_RADIATION,
UV,
WIND_AZIMUT,
WIND_DIR,
WIND_GUST,
WIND_SPEED,
UnitOfDir,
MONTHLY_RAIN,
YEARLY_RAIN,
HOURLY_RAIN,
WEEKLY_RAIN,
)
from .sensors_common import WeatherSensorEntityDescription
from .utils import wind_dir_to_text
SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription(
key=INDOOR_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE,
translation_key=INDOOR_TEMP,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=INDOOR_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.HUMIDITY,
translation_key=INDOOR_HUMIDITY,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=OUTSIDE_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE,
translation_key=OUTSIDE_TEMP,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=OUTSIDE_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer",
device_class=SensorDeviceClass.HUMIDITY,
translation_key=OUTSIDE_HUMIDITY,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=DEW_POINT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer-lines",
device_class=SensorDeviceClass.TEMPERATURE,
translation_key=DEW_POINT,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=BARO_PRESSURE,
native_unit_of_measurement=UnitOfPressure.HPA,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:thermometer-lines",
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
suggested_unit_of_measurement=UnitOfPressure.HPA,
translation_key=BARO_PRESSURE,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
icon="mdi:weather-windy",
translation_key=WIND_SPEED,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=WIND_GUST,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
icon="mdi:windsock",
translation_key=WIND_GUST,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=WIND_DIR,
native_unit_of_measurement=DEGREE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=None,
icon="mdi:sign-direction",
translation_key=WIND_DIR,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=WIND_AZIMUT,
icon="mdi:sign-direction",
value_fn=lambda data: cast("str", wind_dir_to_text(data)),
device_class=SensorDeviceClass.ENUM,
options=list(UnitOfDir),
translation_key=WIND_AZIMUT,
),
WeatherSensorEntityDescription(
key=RAIN,
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=DAILY_RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=DAILY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=HOURLY_RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=HOURLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=WEEKLY_RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=WEEKLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=MONTHLY_RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=MONTHLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=YEARLY_RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=YEARLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=SOLAR_RADIATION,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.IRRADIANCE,
icon="mdi:weather-sunny",
translation_key=SOLAR_RADIATION,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=UV,
name=UV,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UV_INDEX,
icon="mdi:sunglasses",
translation_key=UV,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=CH2_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny",
translation_key=CH2_TEMP,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=CH2_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny",
translation_key=CH2_HUMIDITY,
value_fn=lambda data: cast("int", data),
),
# WeatherSensorEntityDescription(
# key=CH3_TEMP,
# native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.TEMPERATURE,
# suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
# icon="mdi:weather-sunny",
# translation_key=CH3_TEMP,
# value_fn=lambda data: cast(float, data),
# ),
# WeatherSensorEntityDescription(
# key=CH3_HUMIDITY,
# native_unit_of_measurement=PERCENTAGE,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.HUMIDITY,
# icon="mdi:weather-sunny",
# translation_key=CH3_HUMIDITY,
# value_fn=lambda data: cast(int, data),
# ),
# WeatherSensorEntityDescription(
# key=CH4_TEMP,
# native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.TEMPERATURE,
# suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
# icon="mdi:weather-sunny",
# translation_key=CH4_TEMP,
# value_fn=lambda data: cast(float, data),
# ),
# WeatherSensorEntityDescription(
# key=CH4_HUMIDITY,
# native_unit_of_measurement=PERCENTAGE,
# state_class=SensorStateClass.MEASUREMENT,
# device_class=SensorDeviceClass.HUMIDITY,
# icon="mdi:weather-sunny",
# translation_key=CH4_HUMIDITY,
# value_fn=lambda data: cast(int, data),
# ),
WeatherSensorEntityDescription(
key=HEAT_INDEX,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
icon="mdi:weather-sunny",
translation_key=HEAT_INDEX,
value_fn=lambda data: cast("int", data),
),
WeatherSensorEntityDescription(
key=CHILL_INDEX,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
icon="mdi:weather-sunny",
translation_key=CHILL_INDEX,
value_fn=lambda data: cast("int", data),
),
)

View File

@ -5,21 +5,27 @@
"valid_credentials_key": "Provide valid API KEY.",
"valid_credentials_match": "API ID and API KEY should not be the same."
},
"step": {
"user": {
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"title": "Configure access for Weather Station",
"data": {
"API_ID": "API ID / Station ID",
"API_KEY": "API KEY / Password",
"WSLINK": "WSLink API",
"dev_debug_checkbox": "Developer log"
},
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"title": "Configure access for Weather Station",
"data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer."
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer.",
"API_ID": "API ID is the Station ID you set in the Weather Station.",
"API_KEY": "API KEY is the password you set in the Weather Station.",
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
}
}
}
},
"options": {
"error": {
"valid_credentials_api": "Provide valid API ID.",
@ -27,35 +33,58 @@
"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."
},
"step": {
"basic": {
"data": {
"API_ID": "API ID / Station ID",
"API_KEY": "API KEY / Password",
"dev_debug_checkbox": "Developer log"
},
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"title": "Configure credentials",
"data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer."
}
},
"init": {
"title": "Configure SWS12500 Integration",
"description": "Choose what do you want to configure. If basic access or resending data for Windy site",
"menu_options": {
"basic": "Basic - configure credentials for Weather Station",
"windy": "Windy configuration"
},
"title": "Configure SWS12500 Integration"
}
},
"basic": {
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"title": "Configure credentials",
"data": {
"API_ID": "API ID / Station ID",
"API_KEY": "API KEY / Password",
"WSLINK": "WSLink API",
"dev_debug_checkbox": "Developer log"
},
"data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer.",
"API_ID": "API ID is the Station ID you set in the Weather Station.",
"API_KEY": "API KEY is the password you set in the Weather Station.",
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
}
},
"windy": {
"description": "Resend weather data to your Windy stations.",
"title": "Configure Windy",
"data": {
"WINDY_API_KEY": "API KEY provided by Windy",
"windy_enabled_checkbox": "Enable resending data to Windy",
"windy_logger_checkbox": "Log Windy data and responses"
},
"description": "Resend weather data to your Windy stations.",
"title": "Configure Windy"
"data_description": {
"WINDY_API_KEY": "Windy API KEY obtained from https://https://api.windy.com/keys",
"windy_logger_checkbox": "Enable only if you want to send debuging data to the developer."
}
},
"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.",
"data": {
"sensor_to_migrate": "Sensor to migrate",
"trigger_action": "Trigger migration"
},
"data_description": {
"sensor_to_migrate": "Select the correct sensor for statistics migration.\nThe sensor values will be preserved, they will not be recalculated, only the unit in the long-term statistics will be changed.",
"trigger_action": "Trigger the sensor statistics migration after checking."
}
}
}
},
@ -76,7 +105,40 @@
"daily_rain": { "name": "Daily precipitation" },
"solar_radiation": { "name": "Solar irradiance" },
"ch2_temp": { "name": "Channel 2 temperature" },
"ch2_humidity": { "name": "Channel 2 humidity" }
"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": {
"name": "Bearing",
"state": {
"n": "N",
"nne": "NNE",
"ne": "NE",
"ene": "ENE",
"e": "E",
"ese": "ESE",
"se": "SE",
"sse": "SSE",
"s": "S",
"ssw": "SSW",
"sw": "SW",
"wsw": "WSW",
"w": "W",
"wnw": "WNW",
"nw": "NW",
"nnw": "NNW"
}
}
}
},
"notify": {
"added": {
"title": "New sensors for SWS 12500 found.",
"message": "{added_sensors}\n"
}
}
}

View File

@ -7,19 +7,24 @@
},
"step": {
"user": {
"description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem",
"title": "Nastavení přihlášení",
"data": {
"API_ID": "API ID / ID stanice",
"API_ID": "API ID / ID Stanice",
"API_KEY": "API KEY / Heslo",
"wslink": "WSLink API",
"dev_debug_checkbox": "Developer log"
},
"description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem",
"title": "Nastavení přístpu pro metostanici",
"data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer."
"dev_debug_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři.",
"API_ID": "API ID je ID stanice, které jste nastavili v meteostanici.",
"API_KEY": "API KEY je heslo, které jste nastavili v meteostanici.",
"wslink": "WSLink API zapněte, pokud je stanice nastavena na zasílání dat přes WSLink."
}
}
}
},
"options": {
"error": {
"valid_credentials_api": "Vyplňte platné API ID",
@ -27,38 +32,63 @@
"valid_credentials_match": "API ID a API KEY nesmějí být stejné!",
"windy_key_required": "Je vyžadován Windy API key, pokud chcete aktivovat přeposílání dat na Windy"
},
"step": {
"basic": {
"data": {
"API_ID": "API ID / ID Stanice",
"API_KEY": "API KEY / Heslo",
"dev_debug_checkbox": "Developer log"
},
"description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem",
"title": "Nastavení přihlášení",
"data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer."
}
},
"init": {
"title": "Nastavení integrace SWS12500",
"description": "Vyberte, co chcete konfigurovat. Zda přihlašovací údaje nebo nastavení pro přeposílání dat na Windy.",
"menu_options": {
"basic": "Základní - přístupové údaje (přihlášení)",
"windy": "Nastavení pro přeposílání dat na Windy"
},
"title": "Nastavení integrace SWS12500"
"windy": "Nastavení pro přeposílání dat na Windy",
"migration": "Migrace statistiky senzoru"
}
},
"basic": {
"description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem",
"title": "Nastavení přihlášení",
"data": {
"API_ID": "API ID / ID Stanice",
"API_KEY": "API KEY / Heslo",
"wslink": "WSLink API",
"dev_debug_checkbox": "Developer log"
},
"data_description": {
"dev_debug_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři.",
"API_ID": "API ID je ID stanice, které jste nastavili v meteostanici.",
"API_KEY": "API KEY je heslo, které jste nastavili v meteostanici.",
"wslink": "WSLink API zapněte, pokud je stanice nastavena na zasílání dat přes WSLink."
}
},
"windy": {
"description": "Přeposílání dat z metostanice na Windy",
"title": "Konfigurace Windy",
"data": {
"WINDY_API_KEY": "Klíč API KEY získaný z Windy",
"windy_enabled_checkbox": "Povolit přeposílání dat na Windy",
"windy_logger_checkbox": "Logovat data a odpovědi z Windy"
},
"description": "Přeposílání dat z metostanice na Windy",
"title": "Konfigurace Windy"
"data_description": {
"WINDY_API_KEY": "Klíč API KEY získaný z https://https://api.windy.com/keys",
"windy_logger_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři."
}
},
"migration": {
"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ů.",
"data": {
"sensor_to_migrate": "Senzor pro migraci",
"trigger_action": "Spustit migraci"
},
"data_description": {
"sensor_to_migrate": "Vyberte správný senzor pri migraci statistiky. \n Hodnoty senzoru budou zachovány, nepřepočítají se, pouze se změní jednotka v dlouhodobé statistice. ",
"trigger_action": "Po zaškrtnutí se spustí migrace statistiky senzoru."
}
}
}
},
"entity": {
"sensor": {
"indoor_temp": { "name": "Vnitřní teplota" },
@ -68,14 +98,51 @@
"uv": { "name": "UV index" },
"baro_pressure": { "name": "Tlak vzduchu" },
"dew_point": { "name": "Rosný bod" },
"wind_speed": { "name": "Rychlost větrtu" },
"wind_speed": { "name": "Rychlost větru" },
"wind_dir": { "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" }
"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" },
"wind_azimut": {
"name": "Azimut",
"state": {
"n": "S",
"nne": "SSV",
"ne": "SV",
"ene": "VVS",
"e": "V",
"ese": "VVJ",
"se": "JV",
"sse": "JJV",
"s": "J",
"ssw": "JJZ",
"sw": "JZ",
"wsw": "JZZ",
"w": "Z",
"wnw": "ZZS",
"nw": "SZ",
"nnw": "SSZ"
}
}
}
},
"notify": {
"added": {
"title": "Nalezeny nové senzory pro SWS 12500.",
"message": "{added_sensors}\n"
}
}
}

View File

@ -5,21 +5,27 @@
"valid_credentials_key": "Provide valid API KEY.",
"valid_credentials_match": "API ID and API KEY should not be the same."
},
"step": {
"user": {
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"title": "Configure access for Weather Station",
"data": {
"API_ID": "API ID / Station ID",
"API_KEY": "API KEY / Password",
"WSLINK": "WSLink API",
"dev_debug_checkbox": "Developer log"
},
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"title": "Configure access for Weather Station",
"data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer."
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer.",
"API_ID": "API ID is the Station ID you set in the Weather Station.",
"API_KEY": "API KEY is the password you set in the Weather Station.",
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
}
}
}
},
"options": {
"error": {
"valid_credentials_api": "Provide valid API ID.",
@ -27,35 +33,58 @@
"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."
},
"step": {
"basic": {
"data": {
"API_ID": "API ID / Station ID",
"API_KEY": "API KEY / Password",
"dev_debug_checkbox": "Developer log"
},
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"title": "Configure credentials",
"data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer."
}
},
"init": {
"title": "Configure SWS12500 Integration",
"description": "Choose what do you want to configure. If basic access or resending data for Windy site",
"menu_options": {
"basic": "Basic - configure credentials for Weather Station",
"windy": "Windy configuration"
},
"title": "Configure SWS12500 Integration"
}
},
"basic": {
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"title": "Configure credentials",
"data": {
"API_ID": "API ID / Station ID",
"API_KEY": "API KEY / Password",
"WSLINK": "WSLink API",
"dev_debug_checkbox": "Developer log"
},
"data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer.",
"API_ID": "API ID is the Station ID you set in the Weather Station.",
"API_KEY": "API KEY is the password you set in the Weather Station.",
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
}
},
"windy": {
"description": "Resend weather data to your Windy stations.",
"title": "Configure Windy",
"data": {
"WINDY_API_KEY": "API KEY provided by Windy",
"windy_enabled_checkbox": "Enable resending data to Windy",
"windy_logger_checkbox": "Log Windy data and responses"
},
"description": "Resend weather data to your Windy stations.",
"title": "Configure Windy"
"data_description": {
"WINDY_API_KEY": "Windy API KEY obtained from https://https://api.windy.com/keys",
"windy_logger_checkbox": "Enable only if you want to send debuging data to the developer."
}
},
"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.",
"data": {
"sensor_to_migrate": "Sensor to migrate",
"trigger_action": "Trigger migration"
},
"data_description": {
"sensor_to_migrate": "Select the correct sensor for statistics migration.\nThe sensor values will be preserved, they will not be recalculated, only the unit in the long-term statistics will be changed.",
"trigger_action": "Trigger the sensor statistics migration after checking."
}
}
}
},
@ -76,7 +105,44 @@
"daily_rain": { "name": "Daily precipitation" },
"solar_radiation": { "name": "Solar irradiance" },
"ch2_temp": { "name": "Channel 2 temperature" },
"ch2_humidity": { "name": "Channel 2 humidity" }
"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" },
"wind_azimut": {
"name": "Bearing",
"state": {
"n": "N",
"nne": "NNE",
"ne": "NE",
"ene": "ENE",
"e": "E",
"ese": "ESE",
"se": "SE",
"sse": "SSE",
"s": "S",
"ssw": "SSW",
"sw": "SW",
"wsw": "WSW",
"w": "W",
"wnw": "WNW",
"nw": "NW",
"nnw": "NNW"
}
}
}
},
"notify": {
"added": {
"title": "New sensors for SWS 12500 found.",
"message": "{added_sensors}\n"
}
}
}

View File

@ -1,29 +1,107 @@
"""Utils for SWS12500."""
import logging
import math
from pathlib import Path
import sqlite3
from typing import Any
import numpy as np
from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.const import (
UnitOfPrecipitationDepth,
UnitOfTemperature,
UnitOfVolumetricFlux,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.translation import async_get_translations
from .const import DISABLED_BY_DEFAULT, DOMAIN, REMAP_ITEMS
from .const import (
AZIMUT,
DATABASE_PATH,
DEV_DBG,
OUTSIDE_HUMIDITY,
OUTSIDE_TEMP,
REMAP_ITEMS,
REMAP_WSLINK_ITEMS,
SENSORS_TO_LOAD,
WIND_SPEED,
UnitOfDir,
)
_LOGGER = logging.getLogger(__name__)
def update_options(
async def translations(
hass: HomeAssistant,
translation_domain: str,
translation_key: str,
*,
key: str = "message",
category: str = "notify",
) -> str:
"""Get translated keys for domain."""
localize_key = f"component.{translation_domain}.{category}.{translation_key}.{key}"
language = hass.config.language
_translations = await async_get_translations(
hass, language, category, [translation_domain]
)
if localize_key in _translations:
return _translations[localize_key]
return None
async def translated_notification(
hass: HomeAssistant,
translation_domain: str,
translation_key: str,
translation_placeholders: dict[str, str] | None = None,
notification_id: str | None = None,
*,
key: str = "message",
category: str = "notify",
) -> str:
"""Translate notification."""
localize_key = f"component.{translation_domain}.{category}.{translation_key}.{key}"
localize_title = (
f"component.{translation_domain}.{category}.{translation_key}.title"
)
language = hass.config.language
_translations = await async_get_translations(
hass, language, category, [translation_domain]
)
if localize_key in _translations:
if not translation_placeholders:
persistent_notification.async_create(
hass,
_translations[localize_key],
_translations[localize_title],
notification_id,
)
else:
message = _translations[localize_key].format(**translation_placeholders)
persistent_notification.async_create(
hass, message, _translations[localize_title], notification_id
)
async def update_options(
hass: HomeAssistant, entry: ConfigEntry, update_key, update_value
) -> None:
"""Update config.options entry."""
conf = {}
for k in entry.options:
conf[k] = entry.options[k]
conf = {**entry.options}
conf[update_key] = update_value
hass.config_entries.async_update_entry(entry, options=conf)
return hass.config_entries.async_update_entry(entry, options=conf)
def anonymize(data):
@ -31,7 +109,7 @@ def anonymize(data):
anonym = {}
for k in data:
if k not in ("ID", "PASSWORD"):
if k not in {"ID", "PASSWORD", "wsid", "wspw"}:
anonym[k] = data[k]
return anonym
@ -47,37 +125,236 @@ def remap_items(entities):
return items
async def check_disabled(hass: HomeAssistant, items, log: bool = False):
"""Check if we have data for disabed sensors.
def remap_wslink_items(entities):
"""Remap items in query for WSLink API."""
items = {}
for item in entities:
if item in REMAP_WSLINK_ITEMS:
items[REMAP_WSLINK_ITEMS[item]] = entities[item]
If so, then enable senosor.
return items
Returns True if sensor found else False
def loaded_sensors(config_entry: ConfigEntry) -> list | None:
"""Get loaded sensors."""
return config_entry.options.get(SENSORS_TO_LOAD) or []
def check_disabled(
hass: HomeAssistant, items, config_entry: ConfigEntry
) -> list | None:
"""Check if we have data for unloaded sensors.
If so, then add sensor to load queue.
Returns list of found sensors or None
"""
_ER = er.async_get(hass)
eid: str = None
log: bool = config_entry.options.get(DEV_DBG)
entityFound: bool = False
_loaded_sensors = loaded_sensors(config_entry)
missing_sensors: list = []
for disabled in DISABLED_BY_DEFAULT:
for item in items:
if log:
_LOGGER.info("Checking %s", disabled)
if disabled in items:
eid = _ER.async_get_entity_id(Platform.SENSOR, DOMAIN, disabled)
is_disabled = _ER.entities[eid].hidden
_LOGGER.info("Checking %s", item)
if item not in _loaded_sensors:
missing_sensors.append(item)
entityFound = True
if log:
_LOGGER.info("Found sensor %s", eid)
_LOGGER.info("Add sensor (%s) to loading queue", item)
if is_disabled:
if log:
_LOGGER.info("Sensor %s is hidden. Making visible", eid)
_ER.async_update_entity(eid, hidden_by=None)
entityFound = True
return missing_sensors if entityFound else None
elif not is_disabled and log:
_LOGGER.info("Sensor %s is visible.", eid)
return entityFound
def wind_dir_to_text(deg: float) -> UnitOfDir | None:
"""Return wind direction in text representation.
Returns UnitOfDir or None
"""
if deg:
return AZIMUT[int(abs((float(deg) - 11.25) % 360) / 22.5)]
return None
def fahrenheit_to_celsius(fahrenheit: float) -> float:
"""Convert Fahrenheit to Celsius."""
return (fahrenheit - 32) * 5.0 / 9.0
def celsius_to_fahrenheit(celsius: float) -> float:
"""Convert Celsius to Fahrenheit."""
return celsius * 9.0 / 5.0 + 32
def heat_index(data: Any, convert: bool = False) -> UnitOfTemperature:
"""Calculate heat index from temperature.
data: dict with temperature and humidity
convert: bool, convert recieved data from Celsius to Fahrenheit
"""
temp = float(data[OUTSIDE_TEMP])
rh = float(data[OUTSIDE_HUMIDITY])
adjustment = None
if convert:
temp = celsius_to_fahrenheit(temp)
simple = 0.5 * (temp + 61.0 + ((temp - 68.0) * 1.2) + (rh * 0.094))
if ((simple + temp) / 2) > 80:
full_index = (
-42.379
+ 2.04901523 * temp
+ 10.14333127 * rh
- 0.22475541 * temp * rh
- 0.00683783 * temp * temp
- 0.05481717 * rh * rh
+ 0.00122874 * temp * temp * rh
+ 0.00085282 * temp * rh * rh
- 0.00000199 * temp * temp * rh * rh
)
if rh < 13 and (temp in np.arange(80, 112, 0.1)):
adjustment = ((13 - rh) / 4) * math.sqrt((17 - abs(temp - 95)) / 17)
if rh > 80 and (temp in np.arange(80, 87, 0.1)):
adjustment = ((rh - 85) / 10) * ((87 - temp) / 5)
return round((full_index + adjustment if adjustment else full_index), 2)
return simple
def chill_index(data: Any, convert: bool = False) -> UnitOfTemperature:
"""Calculate wind chill index from temperature and wind speed.
data: dict with temperature and wind speed
convert: bool, convert recieved data from Celsius to Fahrenheit
"""
temp = float(data[OUTSIDE_TEMP])
wind = float(data[WIND_SPEED])
if convert:
temp = celsius_to_fahrenheit(temp)
return (
round(
(
(35.7 + (0.6215 * temp))
- (35.75 * (wind**0.16))
+ (0.4275 * (temp * (wind**0.16)))
),
2,
)
if temp < 50 and wind > 3
else temp
)
def long_term_units_in_statistics_meta():
"""Get units in long term statitstics."""
if not Path(DATABASE_PATH).exists():
_LOGGER.error("Database file not found: %s", DATABASE_PATH)
return False
conn = sqlite3.connect(DATABASE_PATH)
db = conn.cursor()
try:
db.execute("""
SELECT statistic_id, unit_of_measurement from statistics_meta
WHERE statistic_id LIKE 'sensor.weather_station_sws%'
""")
rows = db.fetchall()
sensor_units = {
statistic_id: f"{statistic_id} ({unit})" for statistic_id, unit in rows
}
except sqlite3.Error as e:
_LOGGER.error("Error during data migration: %s", e)
finally:
conn.close()
return sensor_units
async def migrate_data(hass: HomeAssistant, sensor_id: str | None = None) -> bool:
"""Migrate data from mm/d to mm."""
_LOGGER.debug("Sensor %s is required for data migration", sensor_id)
updated_rows = 0
if not Path(DATABASE_PATH).exists():
_LOGGER.error("Database file not found: %s", DATABASE_PATH)
return False
conn = sqlite3.connect(DATABASE_PATH)
db = conn.cursor()
try:
_LOGGER.info(sensor_id)
db.execute(
"""
UPDATE statistics_meta
SET unit_of_measurement = 'mm'
WHERE statistic_id = ?
AND unit_of_measurement = 'mm/d';
""",
(sensor_id,),
)
updated_rows = db.rowcount
conn.commit()
_LOGGER.info(
"Data migration completed successfully. Updated rows: %s for %s",
updated_rows,
sensor_id,
)
except sqlite3.Error as e:
_LOGGER.error("Error during data migration: %s", e)
finally:
conn.close()
return updated_rows
def migrate_data_old(sensor_id: str | None = None):
"""Migrate data from mm/d to mm."""
updated_rows = 0
if not Path(DATABASE_PATH).exists():
_LOGGER.error("Database file not found: %s", DATABASE_PATH)
return False
conn = sqlite3.connect(DATABASE_PATH)
db = conn.cursor()
try:
_LOGGER.info(sensor_id)
db.execute(
"""
UPDATE statistics_meta
SET unit_of_measurement = 'mm'
WHERE statistic_id = ?
AND unit_of_measurement = 'mm/d';
""",
(sensor_id,),
)
updated_rows = db.rowcount
conn.commit()
_LOGGER.info(
"Data migration completed successfully. Updated rows: %s for %s",
updated_rows,
sensor_id,
)
except sqlite3.Error as e:
_LOGGER.error("Error during data migration: %s", e)
finally:
conn.close()
return updated_rows

View File

@ -3,10 +3,9 @@
from datetime import datetime, timedelta
import logging
import aiohttp
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
PURGE_DATA,
@ -69,6 +68,9 @@ class WindyPush:
) -> WindyNotInserted | WindySuccess | WindyApiKeyError | None:
"""Verify answer form Windy."""
if self.log:
_LOGGER.info("Windy response raw response: %s", response)
if "NOTICE" in response:
raise WindyNotInserted
@ -78,6 +80,9 @@ class WindyPush:
if "Invalid API key" in response:
raise WindyApiKeyError
if "Unauthorized" in response:
raise WindyApiKeyError
return None
async def push_data_to_windy(self, data):
@ -116,46 +121,44 @@ class WindyPush:
if self.log:
_LOGGER.info("Dataset for windy: %s", purged_data)
session = async_get_clientsession(self.hass, verify_ssl=False)
try:
async with session.get(request_url, params=purged_data) as resp:
status = await resp.text()
try:
self.verify_windy_response(status)
except WindyNotInserted:
# log despite of settings
_LOGGER.error(WINDY_NOT_INSERTED)
async with aiohttp.ClientSession(
connector=aiohttp.TCPConnector(ssl=False), trust_env=True
) as session: # verify_ssl=False; intended to be False
try:
async with session.get(request_url, params=purged_data) as resp:
status = await resp.text()
try:
self.verify_windy_response(status)
except WindyNotInserted:
# log despite of settings
_LOGGER.error(WINDY_NOT_INSERTED)
text_for_test = WINDY_NOT_INSERTED
text_for_test = WINDY_NOT_INSERTED
except WindyApiKeyError:
# log despite of settings
_LOGGER.critical(WINDY_INVALID_KEY)
text_for_test = WINDY_INVALID_KEY
except WindyApiKeyError:
# log despite of settings
_LOGGER.critical(WINDY_INVALID_KEY)
text_for_test = WINDY_INVALID_KEY
update_options(self.hass, self.config, WINDY_ENABLED, False)
except WindySuccess:
if self.log:
_LOGGER.info(WINDY_SUCCESS)
text_for_test = WINDY_SUCCESS
except aiohttp.ClientConnectionError as ex:
_LOGGER.critical("Invalid response from Windy: %s", str(ex))
self.invalid_response_count += 1
if self.invalid_response_count > 3:
_LOGGER.critical(WINDY_UNEXPECTED)
text_for_test = WINDY_UNEXPECTED
update_options(self.hass, self.config, WINDY_ENABLED, False)
self.last_update = datetime.now()
self.next_update = self.last_update + timed(minutes=5)
except WindySuccess:
if self.log:
_LOGGER.info(WINDY_SUCCESS)
text_for_test = WINDY_SUCCESS
except session.ClientError as ex:
_LOGGER.critical("Invalid response from Windy: %s", str(ex))
self.invalid_response_count += 1
if self.invalid_response_count > 3:
_LOGGER.critical(WINDY_UNEXPECTED)
text_for_test = WINDY_UNEXPECTED
update_options(self.hass, self.config, WINDY_ENABLED, False)
self.last_update = datetime.now()
self.next_update = self.last_update + timed(minutes=5)
if self.log:
_LOGGER.info("Next update: %s", str(self.next_update))
if self.log:
_LOGGER.info("Next update: %s", str(self.next_update))
if RESPONSE_FOR_TEST and text_for_test:
return text_for_test
return None

View File

@ -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"

75
firmware_bug.md Normal file
View File

@ -0,0 +1,75 @@
# :bug: Firmware version 1.0 bug
In station's `firmware 1.0` is a bug preventing data to be sent to Home Assistant.
This might be a problem even on firmwares > 1.0.
## :thinking: The issue
Once you have set an URL in station with port as `url:port`, the bug will cause that data will not be sent to the Home Station at all.
![station port](README/station_hint.png)
If you ommit the `:port`, station will send data to designated port `:80` of your URL.
## :adhesive_bandage: Workaround
:bulb: There is a solution to this!
You have to redirect incoming data on `port 80` from station's IP address to `port 8123` or what ever port your instance of Home Assistant is running. To achive this, you have to run `iptables` to redirect ports.
Eg. `192.168.1.2.:any (station) -> 192.168:1.1:80 ---> 192.168.1.1:8123 (HA)`
So I provide a script that will handle this bug. But you must run this script every time you restarted Home Assistant, because `iptables` will not remain on reboot.
Ok, now how to do it?
### Step one
Install [Advanced SSH & Web Terminal](https://github.com/hassio-addons/addon-ssh/blob/main/ssh/DOCS.md) from add-ons. Yes, it **has to be** `Advanced SSH & Web Terminal` not regular `Terminal & SSH`, as regular `Terminal & SSH` do not have such functions, capabilities and privileges.
### Step two
After you have installed `Advanced SSH & Web Terminal` [configure ](https://github.com/hassio-addons/addon-ssh/blob/main/ssh/DOCS.md) it to run on some free port (eg. 23). Make sure you have configured `username` and `authorized_keys`. Please be advised, that this script will not work with `password`.
![ssh_addon_setup](README/addon_ssh_config.png)
### Step three
Open `Web Terminal`. Copy and paste this command:
```
bash <(wget -q -O - https://raw.githubusercontent.com/schizza/SWS-12500-custom-component/main/install_iptables.sh)
```
This will download and run installation script of this workaround.
Follow the instructions in the script.
You will be asked for:
* your station's IP
* Home Assistant IP
* Home Assistatn port
* username in SSH addon (this is one you setup upon installation of the addon)
* SSH port (also one you setup with the addon)
![ssh_run](README/script_run.png)
The scritp will set all you need to modify iptables on every Home Assistant start.
## :warning: Scrip will modify `configuration.yaml` and `automations.yaml` so, please look at it and check that it does not contain any error.
If you have already set `shell_command` in your `configuration.yaml` then you have to transfer created one to your already set one.
### Step four
Script files are stored in your `config/iptables_redirect` directory.
There is also your public key for your SSH server configuration in `ssh` directory. It is important to add public key to your SSH server configuration.
https://github.com/schizza/SWS-12500-custom-component/assets/4604900/bf0a3438-e23b-4425-8de1-6329e4d74319
So you are no all set! :tada:

292
install_iptables.sh Normal file
View File

@ -0,0 +1,292 @@
#!/bin/bash
# Installation script for iptables redirect
RED_COLOR='\033[0;31m'
GREEN_COLOR='\033[0;32m'
GREEN_YELLOW='\033[1;33m'
NO_COLOR='\033[0m'
ST_PORT=80
LINK="https://raw.githubusercontent.com/schizza/SWS-12500-custom-component/main/iptables_redirect.sh"
FILENAME="iptables_redirect.sh"
SCRIPT_DIR="iptables_redirect"
P_HA=true
P_ST=true
declare -a HA_PATHS=(
"/homeassistant"
"$PWD"
"$PWD/config"
"/config"
"$HOME/.homeassistant"
"/usr/share/hassio/homeassistant"
)
function info() { echo -e $2 "${GREEN_COLOR}$1${NO_COLOR}"; }
function warn() { echo -e $2 "${GREEN_YELLOW}$1${NO_COLOR}"; }
function error() {
echo -e "${RED_COLOR}$1${NO_COLOR}"
if [ "$2" != "false" ]; then exit 1; fi
}
function check() {
echo -n "Checking dependencies: '$1' ... "
if [ -z "$(command -v "$1")" ]; then
error "not installed" $2
false
else
info "OK."
true
fi
}
function validate_ip() {
if [[ "$1" =~ ^(([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\.){3}([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))$ ]]; then
true
else
false
fi
}
function validate_num() {
if [[ "$1" =~ ^[0-9]+$ ]]; then true; else false; fi
}
function validate_dest() {
echo -n "Validating host '$1' ... "
if ping -c 2 $1 >/dev/null 2>&1; then
info "OK"
true
else
error "cannot reach" false
false
fi
}
function exit_status() {
# argv 1 - status
# 2 - called function
# 3 - error message
# 4 - success message
# 5 - exit on error bool
if [ $1 -ne 0 ]; then
warn "$2 exited with error: $1"
error "$3" $5
else
info "$4"
fi
}
function cont() {
while true; do
warn "$1"
warn "Do you want to continue? [y/N]: " -n
read -n 1 YN
YN=${YN:-N}
case $YN in
[Yy])
echo -e "\n"
return 0
;;
[Nn]) error "\nExiting." ;;
*) error "\nInvalid response.\n" false ;;
esac
done
}
echo
echo "**************************************************************"
echo "* *"
echo -e "* ${GREEN_YELLOW}Installation for iptables_redirect.sh ${NO_COLOR} *"
echo "* *"
echo "**************************************************************"
echo
check "wget"
check "sed"
check "ping" false && { PING=true; } || { PING=false; }
check "ssh-keygen" false && { KEYGEN=true; } || { KEYGEN=false; }
echo -n "Trying to find Home Assitant ... "
for _PATH in "${HA_PATHS[@]}"; do
if [ -n "$HA_PATH" ]; then
break
fi
if [ -f "$_PATH/.HA_VERSION" ]; then
HA_PATH="$_PATH"
fi
done
COMPLETE_PATH="$HA_PATH/$SCRIPT_DIR"
FILENAME="$COMPLETE_PATH/$FILENAME"
[ -z $HA_PATH ] && { error "Home Assistant not found!"; }
info "found at $HA_PATH"
[ -d $COMPLETE_PATH ] && {
warn "Previous version of script exists ... removing directory ($COMPLETE_PATH)"
rm -r $COMPLETE_PATH
}
mkdir -p $COMPLETE_PATH
while true; do
read -r -p "Your station's IP: " ST_IP
if validate_ip $ST_IP; then break; fi
warn "Provide valid IP address."
done
while true; do
read -r -p "Home Assistant's IP: " HA_IP
if validate_ip $HA_IP; then break; fi
warn "Provide valid IP address."
done
while true; do
read -r -p "Home Assistant's port [8123]: " HA_PORT
HA_PORT=${HA_PORT:-8123}
if validate_num $HA_PORT && ((HA_PORT >= 1 && HA_PORT <= 65535)); then
break
fi
warn "Provide valid port number."
done
read -r -p "SSH server username: " SSH_USER
read -r -p "SSH server port: " SSH_PORT
if $PING; then
validate_dest $HA_IP || {
cont "Home Assistant host is unreachable."
P_HA=false
}
validate_dest $ST_IP || {
cont "Station is unreachable."
P_ST=false
}
fi
echo -n "Downloading 'iptables_redirect.sh' ... "
wget -q -O - "$LINK" | sed -e "s/\[_STATION_IP_\]/$ST_IP/" \
-e "s/\[_HA_\]/$HA_IP/" \
-e "s/\[_SRC_PORT_\]/$ST_PORT/" \
-e "s/\[_DST_PORT_\]/$HA_PORT/" >$FILENAME
exit_status $? "wget" \
"Could not download 'iptables_redirect.sh'." \
"iptables_redirect.sh downloaded successffully."
if $KEYGEN; then
echo -n "Generating ssh key-pairs ... "
mkdir -p "$COMPLETE_PATH/ssh"
ssh-keygen -t ecdsa -b 521 -N "" -f "$COMPLETE_PATH/ssh/ipt_dsa" -q
exit_status $? "ssh-keygen" \
"Could not create ssh key-pairs." \
"SSH key-pairs created successfully (at $COMPLETE_PATH/ssh/)" \
false
fi
echo -n "Creating 'exec.sh' script ... "
cat >$COMPLETE_PATH/exec.sh <<-EOF
#!/bin/bash
cat iptables_redirect/iptables_redirect.sh | ssh -i iptables_redirect/ssh/ipt_dsa -o StrictHostKeyChecking=no -p $SSH_PORT -l $SSH_USER $HA_IP /bin/zsh
EOF
exit_status $? "cat" \
"Could not write '$COMPLETE_PATH/exec.sh'" \
"OK."
echo -n "Setting 'exec.sh' script right privileges ... "
chmod -f a+rx "$COMPLETE_PATH/exec.sh"
exit_status $? "chmod" \
"Filed to set +x on exec.sh" \
"OK."
echo -n "Setting 'iptables_redirect.sh' script right privileges ... "
chmod -f a+rx "$COMPLETE_PATH/iptables_redirect.sh"
exit_status $? "chmod" \
"Filed to set +x on exec.sh" \
"OK."
echo -n "Creating 'runscript' ... "
cat >$COMPLETE_PATH/runscript <<-"EOF"
#!/bin/bash
SCRIPT=$(find /homeassistant -name "iptables_redirect.sh" | sed -n 1p)
sudo $SCRIPT
EOF
exit_status $? "cat" \
"Could not write 'runscript'" \
"OK."
echo -n "Modifying configuration.yaml ... "
cat >>$HA_PATH/configuration.yaml <<EOF
shell_command:
iptables_script: iptables_redirect/exec.sh
EOF
exit_status $? "cat" \
"Could not modify configuration.yaml" \
"OK." \
false
echo -n "Modifying automations.yaml ... "
cat >>$HA_PATH/automations.yaml <<EOF
- id: '1714725977432'
alias: Run iptables_redirect on HA start
description: On every start we will run iptables_redirect script to ensure accepting
data from station with firmware 1.0
trigger:
- platform: homeassistant
event: start
condition: []
action:
- service: shell_command.iptables_script
metadata: {}
data: {}
mode: single
EOF
exit_status $? "cat" \
"Could not modify automations.yaml" \
"OK." \
false
echo "Executing 'iptables_redirecet.sh' ... "
/bin/bash $FILENAME
FIRST_RUN=$?
exit_status $FIRST_RUN "iptables_redirect.sh" \
"iptables_redirect scritp did not run successfully.\n But is installed in $FILENAME.\n Please run it again a look at the log." \
"First run of 'iptables_redirect.sh' was successfful. Your iptables are set." \
false
info "\nYour configuration:"
info " Home Assistant at: $HA_PATH"
info " Home Assistant server at: $HA_IP:$HA_PORT" -n
if $PING; then
if $P_HA; then info " (ping OK)"; else error " (unreachable)" false; fi
else
error " (not tested)" false
fi
info " Station at: ${ST_IP}:$ST_PORT" -n
if $PING; then
if $P_ST; then info " (ping OK)"; else error " (unreachable)" false; fi
else
error " (not tested)" false
fi
info " First run of 'iptables_redirect.sh' script " -n
[ $FIRST_RUN -ne 0 ] && { error " failed." false; } || { info " passed."; }
info " SSH pub_key: at $COMPLETE_PATH/ssh/ipt_dsa.pub"

90
iptables_redirect.sh Executable file
View File

@ -0,0 +1,90 @@
#!/bin/bash
# Script for frowarding SWS 12500 station's destination port 80
# to your Home Assistant's instance port (8123)
#
# Workaround for station's firmware 1.0 bug
#
#
# Script pro přesměrování portu pro stanici SWS12500
STATION_IP=[_STATION_IP_]
HA=[_HA_]
SRC_PORT=[_SRC_PORT_]
DST_PORT=[_DST_PORT_]
INSTALL_IPTABLES=0
APK_MISSING=0
RED_COLOR='\033[0;31m'
GREEN_COLOR='\033[0;32m'
GREEN_YELLOW='\033[1;33m'
NO_COLOR='\033[0m'
function info() { echo -e "${GREEN_COLOR}$1${NO_COLOR}"; }
function warn() { echo -e "${GREEN_YELLOW}$1${NO_COLOR}"; }
function error() {
echo -e "${RED_COLOR}$1${NO_COLOR}"
if [ "$2" != "false" ]; then exit 1; fi
}
function check() {
echo -n "Checking dependencies: '$1' ... "
if [ -z "$(command -v "$1")" ]; then
error "not installed" $2
return 1
fi
info "OK."
return 0
}
echo
echo "**************************************************************"
echo "* *"
echo -e "* ${GREEN_YELLOW}Running iptables forward for port $SRC_PORT -> $DST_PORT ${NO_COLOR} *"
echo "* *"
echo "**************************************************************"
echo
# Check for dependencies
check "iptables" false
INSTALL_IPTABLES=$?
check "apk" false
APK_MISSING=$?
if [ $APK_MISSING -eq 1 ] && [ $INSTALL_IPTABLES -eq 1 ]; then
error "Could not install and run iptables.\n'apk' installer is missing and 'iptables' are not installed.\n"
fi
if [ $INSTALL_IPTABLES -eq 1 ] && [ $APK_MISSING -eq 0 ]; then
echo -n "Installing 'iptables' ... ${RUNINSTALL[@]} ... "
sudo apk add iptables
EXIT_STATUS=$?
if [ $EXIT_STATUS -ne 0 ]; then
warn "apk error code: $EXIT_STATUS"
error "Installation of iptables failed!"
else
info "'iptables' installed successfully."
fi
fi
declare -a RULE=(PREROUTING -t nat -s $STATION_IP -d $HA -p tcp -m tcp --dport $SRC_PORT -j REDIRECT --to-ports $DST_PORT)
echo -n "Chceking for existing rule in iptables ... "
sudo iptables -C ${RULE[@]} 2>/dev/null
if [ $? -eq 0 ]; then
warn "Rule is already present in PREROUTING chain."
else
info "not found."
echo -n "Inserting iptables rule to PREROUTING chain ... "
sudo iptables -I ${RULE[@]} 2>/dev/null
fi
EXIT_STATUS=$?
if [ $EXIT_STATUS -ne 0 ]; then
warn "iptables error code: ${EXIT_STATUS} "
error "Rule could not be added!"
else
info "OK."
fi
info "iptables are now set to redirect incoming connections from $STATION_IP:Any -> $HA:$SRC_PORT to $HA:$DST_PORT"