├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ └── feature-request.yml
└── workflows
│ └── publish.yml
├── .gitignore
├── CHANGELOG.md
├── CHANGELOG.old.md
├── DOCS.md
├── Dockerfile
├── LATEST_CHANGES.md
├── LICENSE
├── README.md
├── config.json
├── config_example_AIR_SKY.yaml
├── config_example_TEMPEST.yaml
├── docker-compose.yml
├── hass-weatherflow2mqtt.code-workspace
├── icon.png
├── logo.png
├── repository.json
├── requirements.txt
├── setup.py
├── test_requirements.txt
├── tox.ini
└── weatherflow2mqtt
├── __init__.py
├── __main__.py
├── __version__.py
├── const.py
├── forecast.py
├── helpers.py
├── sensor_description.py
├── sqlite.py
├── translations
├── da.json
├── de.json
├── en.json
├── fr.json
├── nl.json
├── se.json
└── si.json
└── weatherflow_mqtt.py
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: File a bug report for Weatherflow2MQTT
3 | labels: ["bug"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thanks for taking the time to fill out this bug report!
9 | - type: input
10 | id: ha-version
11 | attributes:
12 | label: Home Assistant Version?
13 | description: What version of Home Assistant are you running?
14 | placeholder: "2021.12.3"
15 | validations:
16 | required: true
17 | - type: dropdown
18 | id: addon-selfhosted
19 | attributes:
20 | label: Add-On or Self Hosted?
21 | description: Is Weatherflow2MQTT running as a HA Supervised Add-On or a Self managed container?
22 | options:
23 | - HA Supervised (Add-On)
24 | - Self managed Container
25 | validations:
26 | required: true
27 | - type: input
28 | id: addon-version
29 | attributes:
30 | label: Weatherflow2MQTT version?
31 | description: What version of Weatherflow2MQTT is causing the error?
32 | placeholder: "3.0.4"
33 | validations:
34 | required: true
35 | - type: textarea
36 | id: what-happened
37 | attributes:
38 | label: What happened?
39 | description: Also tell us, what did you expect to happen?
40 | placeholder: Describe the bug
41 | validations:
42 | required: true
43 | - type: textarea
44 | id: logs
45 | attributes:
46 | label: Relevant log output
47 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
48 | render: shell
49 | validations:
50 | required: false
51 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Ask for a new feature for Weatherflow2MQTT
3 | labels: ["enhancement"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thanks for taking the time to fill out this feature request.
9 | - type: textarea
10 | id: new-feature
11 | attributes:
12 | label: New Feature
13 | description: Please describe in detail what feature you would like to have.
14 | placeholder: Describe the feature
15 | validations:
16 | required: true
17 | - type: textarea
18 | id: additional-context
19 | attributes:
20 | label: Additional context
21 | description: Add any other context or screenshots about the feature request here.
22 | validations:
23 | required: false
24 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - '**/README.md'
9 | - '**/CHANGELOG.md'
10 | - '**/config_example.yaml'
11 |
12 |
13 | jobs:
14 | publish-weatherflow2mqtt-image:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 |
20 | - name: Login to GitHub Container Registry
21 | uses: docker/login-action@v1
22 | with:
23 | registry: ghcr.io
24 | username: ${{ github.actor }}
25 | password: ${{ secrets.GITHUB_TOKEN }}
26 |
27 | - name: Build the WeatherFlow2MQTT Docker image
28 | run: |
29 | VERSION=$(cat VERSION)
30 | echo VERSION=$VERSION
31 |
32 | docker build . --tag ghcr.io/briis/hass-weatherflow2mqtt:latest --tag ghcr.io/briis/hass-weatherflow2mqtt:$VERSION
33 | docker create -v $(pwd):/config -p 50222:50222/udp ghcr.io/briis/hass-weatherflow2mqtt:$VERSION
34 | docker create -v $(pwd):/config -p 50222:50222/udp ghcr.io/briis/hass-weatherflow2mqtt:latest
35 | docker push ghcr.io/briis/hass-weatherflow2mqtt:$VERSION
36 | docker push ghcr.io/briis/hass-weatherflow2mqtt:latest
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 | test.py
131 | config.yaml
132 | build_run.sh
133 | data.json
134 | storage.json
135 | .storage.json
136 | .lightning.data
137 | forecast.json
138 | weatherfunctions.js
139 | .pressure.data
140 | weatherflow2mqtt.db
141 | config_bck.yaml
142 | max_min_day.sql
143 | upload_docker.sh
144 | .DS_Store
145 | upload_docker
146 | build_run
147 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [3.2.2] - 2023-10-08
6 |
7 | ### BREAKING Announcement
8 |
9 | As there is now a `Home Assistant Core` integration for WeatherFlow which uses the UDP API, I had to make a [new Integration](https://github.com/briis/weatherflow_forecast) that uses the REST API, with a different name (WeatherFlow Forecast). The new integration is up-to-date with the latest specs for how to create a Weather Forecast, and also gives the option to only add the Forecast, and no additional sensors.
10 |
11 | There is no *Weather Entity* in Home Assistant for MQTT, so after attributes are deprecated in Home Assistant 2024.3, there is no option to add the Forecast to Home Assistant.
12 | As a consequence of that, I have decided to remove the ability for this Add-On to add Forecast data to MQTT and Home Assistant. This Add-On will still be maintained, but just without the option of a Forecast - meaning it will be 100% local.
13 | If you want the forecast in combination with this Add-On, install the new integration mentioned above, just leave the *Add sensors* box unchecked.
14 |
15 | There is not an exact date for when this will happen, but it will be before end of February 2024.
16 |
17 | ### Changes
18 |
19 | - Added Slovenian language file. This was unfortunately placed in a wrong directory and as such it was not read by the integration. Fixing issue #236
20 | - Fixed issue #244 with deprecated forecast values. Thank you @mjmeli
21 | - Corrected visibility imperial unit from nautical mile (nmi) to mile (mi)
22 |
23 | ## [3.2.1] - 2023-08-31
24 |
25 | ### Changes
26 |
27 | - Some stations do not get the Sea Level Pressure and/or the UV value in the Hourly Forecast. It is not clear why this happens, but the issue is with WeatherFlow. The change implemented here, ensures that the system does not break because of that. If not present a 0 value is returned.
28 | This fixes Issue #234, #238 and maybe also #239
29 |
30 | ## [3.2.0] - 2023-08-29
31 |
32 | ### Changes
33 |
34 | - Fixed wrong type string in the `rain_type` function, so that it should now also get a string for Heavy Rain. Thanks to @GlennGoddard for spotting this. Closing #205
35 | - Changing units using ^ to conform with HA standards
36 | - Adding new device classes to selected sensors. (Wind Speed, Distance, Irradiation, Precipiation etc.)
37 | - Closing #198 and #215, by trying to ensure that correct timezone and unit system is always set
38 | - Added swedish translation. Thank you to @Bo1jo
39 | - Bumped docker image to `python:3.11-slim-buster` and @pcfens optimized the `Dockerfile`` to create a faster and smaller image.
40 | - Bumped all dependency modules to latest available version
41 | - Thanks @quentinmit the following improvements have been made, that makes it easier to run the program without Docker in a more traditional `setuptools` way.
42 | - Translations are installed and loaded as package data
43 | - The no-longer-supported asyncio PyPI package is removed from requirements.txt
44 | - Pint 0.20 and 0.21 are supported (also requires the pyweatherflowudp patch I sent separately)
45 | - @prigorus added the Slovenian translation
46 |
47 |
--------------------------------------------------------------------------------
/CHANGELOG.old.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file.
3 |
4 |
5 | ## [3.1.6] - 2023-01-08
6 |
7 | ### Fixed
8 |
9 | - Fixed the Tempest battery % calculation to show a more accurate calculation.
10 |
11 | ## [3.1.4] - 2022-12-29
12 |
13 | ### Added
14 |
15 | - Added new sensor `fog_probability` which returns the probability for fog based on current conditions. Thanks to @GlennGoddard for creating the formula.
16 | - Added new sensor `snow_probability` which returns the probability of snow based on current conditions. Thanks to @GlennGoddard for creating the formula.
17 | - Added new version attribute to the `sensor.hub_SERIAL_NUMBER_status`. This attribute will always hold the current version of the Docker Container.
18 |
19 | ### Changed
20 |
21 | - Updated the French Translations. Thank you @MichelJourdain
22 | - Issue #194. Adjusted Tempest minimum battery level to 2.11 V. The specs say that min is 1.8 V but experience show that below 2.11 the Tempest device does not work. So battery percent will now use this new min value.
23 |
24 | ## [3.1.3] - 2022-08-20
25 |
26 | ### Added
27 |
28 | - Added support for the Dutch Language. Big thanks to @vdbrink for adding this.
29 |
30 | ### Changed
31 |
32 | - Updated README to clarify how to get Station ID.
33 |
34 | ## [3.1.1] - 2022-07-12
35 |
36 | ### Fixed
37 |
38 | - Fixed Issue #163 Bumped `pyweatherflowudp` to V1.4.1. Thank you to @natekspencer.
39 | - Adjusted logic for calculate_sea_level_pressure to match WeatherFlow (https://weatherflow.github.io/Tempest/api/derived-metric-formulas.html#sea-level-pressure)
40 |
41 | ## [3.1.0] - 2022-07-03
42 |
43 | ### Changed
44 |
45 | - Bumped `pyweatherflowudp` to V1.4.0. Thank you to @natekspencer for keeping this module in good shape.
46 | - Adjusted logic for `wind_direction` and `wind_direction_cardinal` to report based on the last wind event or observation, whichever is most recent (similar to `wind_speed`)
47 | - Added properties for `wind_direction_average` and `wind_direction_average_cardinal` to report only on the average wind direction
48 | - Handle UnicodeDecodeError during message processing
49 | - Bump Pint to ^0.19
50 | - Bumped Docker image to `python:3.10-slim-buster`, to get on par with Home Assistant.
51 |
52 | **Breaking Changes**:
53 |
54 | - The properties `wind_direction` and `wind_direction_cardinal` now report based on the last wind event or observation, whichever is most recent. If you want the wind direction average (previous logic), please use the properties `wind_direction_average` and `wind_direction_average_cardinal`, respectively
55 | - The default symbol for `rain_rate` is now `mm/h` instead of `mm/hr` due to Pint 0.19 - https://github.com/hgrecco/pint/pull/1454
56 |
57 | ### Added
58 |
59 | - Added new sensor `solar_elevation` which returns Sun Elevation in Degrees with respect to the Horizon. Thanks to @GlennGoddard for creating the formula.
60 | **NOTE**: If you are not running this module as a HASS Add-On, you must ensure that environment variables `LATITUDE` and `LONGITUDE` are set in the Docker Startup command, as these values are used to calculate this value.
61 | - Added new sensor `solar_insolation` which returns Estimation of Solar Radiation at current sun elevation angle. Thanks to @GlennGoddard for creating the formula.
62 | - Added new sensors `zambretti_number` and `zambretti_text`, which are sensors that uses local data to create Weather Forecast for the near future. In order to optimize the forecast, these values need the *All Time High and Low Sea Level Presurre*. Per default these are set to 960Mb for Low and 1050Mb for High when using Metric units - 28.35inHg and 31.30inHg when using Imperial units. They can be changed by adding the Environment Variables `ZAMBRETTI_MIN_PRESSURE` and `ZAMBRETTI_MAX_PRESSURE` to the Docker Start command. Thanks to @GlennGoddard for creating the formula and help with all the testing on this.
63 |
64 | ## [3.0.8] - 2022-06-06
65 |
66 | ### Fixed
67 |
68 | - BUGFIX: Changed spelling error in `en.json` file. Tnanks to @jazzyisj
69 | - BUGFIX: Bump `pyweatherflowudp` to V1.3.1 to handle `up_since` oscillation on devices
70 |
71 | ### Added
72 |
73 | - NEW: Added Latitude and Longitude environment variables. These will be used in later additions for new calculated sensors. If you run the Supervised Add-On, then these will be provided automatically by Home Assistent. If you run a standalone docker container, you must add: `-e LATITUDE=your_latiude e- LONGITUDE=your_longitude` to the Docker startup command.
74 |
75 |
76 | ## [3.0.7] - 2022-01-12
77 |
78 | ### Fixed
79 |
80 | - BUGFIX: Don't add battery mode sensor for Air/Sky devices. If the sensor was already created, you will have to delete it manually in MQTT.
81 | - BUGFIX: In rare occasions the forecast icon is not present in data supplied from WeatherFlow. Will now be set to *Cloudy* as default.
82 |
83 | ## [3.0.6] - 2021-12-30
84 |
85 | ### Fixed
86 |
87 | - BUGFIX: Handle MQTT qos>0 messages appropriately by calling loop_start() on the MQTT client
88 | - See https://github.com/eclipse/paho.mqtt.python#client
89 | - Fixing Issue #46.
90 |
91 | ## [3.0.5] - 2021-12-24
92 |
93 | ### Changes
94 |
95 | - Add cloud base altitude and freezing level altitude sensors
96 | - Migrate sea level pressure sensor to use the pyweatherflowudp calculation
97 | - Bump pyweatherflowudp to 1.3.0
98 |
99 | ## [3.0.4] - 2021-12-22
100 |
101 | ### Changes
102 |
103 | - Add a debug log when updating forecast data and set qos=1 to ensure delivery
104 | - Add another debug log to indicate next forecast update
105 | - Docker container is now using the `slim-buster` version of Ubuntu instead of the full version, reducing the size of the Container to one third of the original size.
106 |
107 |
108 | ## [3.0.3] - 2021-12-15
109 |
110 | ### Changes
111 |
112 | - Add discord link to README
113 | - Remove obsolete references to obs[] data points
114 |
115 | ### Fixed
116 |
117 | - Handle timestamps in their own event so they are only updated when a value exists. Fixing #117
118 | - Fix high/low update
119 |
120 | ## [3.0.2] - 2021-12-11
121 |
122 | ### Changes
123 |
124 | - Uses rain_rate from the pyweatherflowudp package now that it is available. This should solve an issue with Heavy Rain #100 where, when on imperial, rain rate was first converted to in/hr but then passed to rain intensity which is based on a mm/hr rate
125 | - Sends the forecast data to MQTT with a retain=True value so that it can be restored on a Home Assistant restart instead of waiting for the next forecast update
126 | - Reduces the loops for setting up the hub sensor by separating device and hub sensors
127 | - Handles unknown timestamps for last lightning/rain so that it shows up as "Unknown" instead of "52 years ago" when there is no value
128 | - Changes the number of Decimal places for Air Density to 5.
129 |
130 |
131 | ## [3.0.1] - 2021-12-10
132 |
133 | ### Fixed
134 |
135 | - Issue #109. Adding better handling of missing data points when parsing messages which may occure when the firmware revision changes, to ensure the program keeps running.
136 |
137 | ## [3.0.0] - 2021-12-10
138 |
139 | This is the first part of a major re-write of this Add-On. Please note **this version has breaking changes** so ensure to read these release notes carefully. Most of the changes are related to the internal workings of this Add-On, and as a user you will not see a change in the data available in Home Assistant. However the Device structure has changed, to make it possible to support multiple devices.
140 |
141 | I want to extend a big THANK YOU to @natekspencer, who has done all the work on these changes.
142 |
143 | The UDP communication with the WeatherFlow Hub, has, until this version, been built in to the Add-On. This has now been split out to a separate module, which makes it a lot easier to maintain new data points going forward.
144 | @natekspencer has done a tremendous job in modernizing the [`pyweatherflowudp`](https://github.com/briis/pyweatherflowudp) package. This package does all the UDP communication with the WeatherFlow hub and using this module, we can now remove all that code from this Add-On.
145 |
146 | ### Breaking Changes
147 |
148 | - With the support for multiple devices per Hub, we need to ensure that we know what data comes from what device. All sensors will as minimum get a new name. Previously sensors were named `WF Sensor Name` now they will be named `DEVICE SERIAL_NUMBER Sensor Name`. The entity_id will be `sensor.devicename_serialnumber_SENSORNAME`.
149 | - For existing installations with templates, automations, scripts or more that reference the previous `sensor.wf_` entities, it may be easier to perform the following steps after updating to this release than finding everywhere they have been used:
150 | 1. Ensure you have advanced mode turned on for your user profile
[](https://my.home-assistant.io/redirect/profile/)
151 | 2. Navigate to integrations
[](https://my.home-assistant.io/redirect/integrations/)
152 | 3. Click on "# devices" under your MQTT integration
153 | 4. Click on the "WeatherFlow2MQTT" device (or whatever you renamed it to) and then delete it
154 | 5. Go back to the MQTT devices and click on one of your sensor devices (Air, Sky, Tempest)
155 | 6. Edit the device name and set it to "WF"
156 | 7. Click on "UPDATE"
157 | 8. A popup will ask if you also want to rename the entity IDs (requires advanced mode as stated in step 1). Click on "RENAME" and Home Assistant will rename all the entities to `sensor.wf_`.
158 | - If any entity IDs clash, you will get an error, but you can handle these individually as you deem necessary
159 | 9. You can then rename the device back to your sensor name. Just click "NO" on the popup asking to change the entity IDs or you will have to repeat the process
160 | 10. Repeat steps 5-9 for each sensor (mostly applicable to air & sky setups).
161 | - Status sensor is now a timestamp (referencing the up_since timestamp of the device) instead of the "humanized" time string since HA takes care of "humanizing" on the front end. This reduces state updates on the sensor since it doesn't have to update every time the uptime seconds change
162 | - `device`\_status (where device is hub, air, sky or tempest) is now just status
163 | - similarly, battery\_`sensor`, battery_level\_`sensor` and voltage\_`sensor` are now just battery, battery_level and voltage, respectively
164 |
165 | ### Changes
166 |
167 | - Multiple devices are now created in mqtt (one for each device)
168 | - Removes the TEMPEST_DEVICE environment variable/config option since we no longer need a user to tell us the type of device
169 | - You will get a warning in the supervisor logs about the TEMPEST_DEVICE option being set until it is removed from your add-on configuration yaml.
170 |
171 | ## [2.2.5] - 2021-12-04
172 |
173 | ### Fixed
174 |
175 | - With HA V2021.12 all date and time values need to be in utc time with timezone information. #105
176 |
177 | ## [2.2.4] - 2021-11-20
178 |
179 | ### Changed
180 |
181 | - @natekspencer did inital work to implement a easier to manage class structure to ease future development.
182 | - **BREAKING CHANGE** If you run the Non-Supervised mode of the program, you must make a change in your docker configuration to ensure you point to the same data directory as before. Change this `-v $(pwd): /usr/local/config` to this `-v $(pwd): /data`
183 |
184 | ## [2.2.3] - 2021-11-17
185 |
186 | ### Changed
187 |
188 | - @natekspencer optimized the unit conversion code.
189 | - New Logos for the Home Assistant Add-On
190 |
191 | ## [2.2.2] - 2021-11-15
192 |
193 | ### Changed
194 |
195 | - Issue #93. A user reported that Temperature sensors creates an error when being exported to `homekit`. This is not a consistent error, but might be due to unicoding of the degree symbol. The `unit_of_measurement` value has now been changed so that it reflects the constants from Home Assistant.
196 |
197 | ## [2.2.1] - 2021-11-13
198 |
199 | @natekspencer further enhanced the Home Assistant Add-On experience and made this more compliant with the way the Add-On is setup. Also he added a new option to filter out sensors _you do not want_, plus a few other great things you can read about below. Thank you @natekspencer.
200 |
201 | ### Changed
202 |
203 | - **BREAKING CHANGE** Move mapped volume from /usr/local/config to /data to support supervisor. If you are not running the Home Assistant supervised version, then you will need change this `v $(pwd):/usr/local/config` to this `v $(pwd):/data`.
204 | - Move configuration defaults to code and gracefully handle retrieval
205 | - Cleanup environment variables in Dockerfile since they are now handled in code
206 | - Simplify config loading between environment/supervisor options
207 | - Remove TZ option from HA supervisor configuration since it should be loaded from HA
208 |
209 | ### Added
210 |
211 | - Add options for FILTER_SENSORS and INVERT_FILTER to avoid having to load a config.yaml file in HA
212 | - Add a list of obsolete sensors that can be used to handle cleanup of old sensors when they are deprecated and removed
213 |
214 | ## [2.2.0] - 2021-11-10
215 |
216 | ### Changed
217 |
218 | - **BREAKING CHANGE** The sensor `sensor.wf_uptime` has been renamed to `sensor.wf_hub_status`. This sensor will now have more attributes on the status of the Hub, like Serial Number, FW Version etc.
219 |
220 | ### Added
221 |
222 | - Thanks to @natekspencer this image can now be installed and managed from the Home Assistant Add-On store. This is not part of the default store yet, but to use it from the Add-On store, just click the button 'ADD REPOSITORY' in the top of the README.md file. **NOTE** Remember to stop/remove the container running outside the Add-On store before attempting to install.
223 | - Depending on what HW you have, there will be 1 or 2 new sensors created, called either `sensor.wf_tempest_status` (If you have a Tempest device) or `sensor.wf_air_status` and `sensor.wf_sky_status`. The state of the sensors will display the Uptime of each device, and then there will attributes giving more details about each HW device.
224 |
225 | ## 2.1.1
226 |
227 | **Release Date**: October 17th, 2021
228 |
229 | ### Changes in release 2.1.1
230 |
231 | * `NEW`: Discussion #83, added new sensor called `sensor.wf_absolute_humidity`, which shows the actual amount of water in volume of air. Thank you to @GlennGoddard for creating the formula.
232 |
233 | ## Version 2.1.0
234 |
235 | **Release Date**: October 11th, 2021
236 |
237 | ### Changes in release 2.1.0
238 |
239 | * `FIX`: Issue #78, wrong Hex code used for decimal symbols.
240 | * `NEW`: Issue #77. Added new sensor called `sensor.wf_battery_mode`. This sensor reports a mode between 0 and 3, and the description for the mode is added as an attribute to the sensor. Basically it shows how the Tempest device operates with the current Voltage. You can read more about this on the [WeatherFlow Website](https://help.weatherflow.com/hc/en-us/articles/360048877194-Solar-Power-Rechargeable-Battery). **This sensor is only available for Tempest devices**
Thank you to @GlennGoddard for creating the formula.
241 | * `NEW`: Issue #81. Added `state_class` attributes to all relevant sensors, so that they can be used with [Long Term Statistics](https://www.home-assistant.io/blog/2021/08/04/release-20218/#long-term-statistics). See the README file for a list of supported sensors.
242 | * `NEW`: Discussion #83. Added new sensor `sensor.wf_rain_intensity`. This sensor shows a descriptive text about the current rain rate. See more on the [Weatherflow Community Forum](https://community.weatherflow.com/t/rain-intensity-values/806). The French and German translations are done by me, so they might need some checking to see if they are correct.
243 |
244 | ## Version 2.0.17
245 |
246 | **Release Date**: August 18th, 2021
247 |
248 | ### Changes in release 2.0.17
249 |
250 | `NEW`: Battery Percent sensors added. There will 2 new sensors if using AIR & SKY devices and 1 new if using the Tempest device. At the same time, the previous `battery` sensor name is now renamed to `voltage` as this is a more correct description of the value. Thanks to @GlennGoddard for creating the formula to do the conversion.
251 | **BREAKING** If you already have a running installation, you will have to manually rename two sensors entity_id, to correspond to the new naming: If you have a Tempest device rename `sensor.wf_battery_tempest` to `sensor.wf_voltage_tempest` and `sensor.wf_battery_tempest_2` to `sensor.wf_battery_tempest`. If you have AIR and SKY devices rename `sensor.wf_battery_sky` to `sensor.wf_voltage_sky` and `sensor.wf_battery_sky_2` to `sensor.wf_battery_sky`
252 |
253 | `NEW`: Wet Bulb Globe Temperature. The WetBulb Globe Temperature (WBGT) is a measure of the heat stress in direct sunlight. Thanks to @GlennGoddard for creating the formula
254 |
255 | `FIX`: Fixing issue #71. Device status was reported wrong. Thank you to @WM for catching this and proposing the fix.
256 |
257 | `CHANGE`: Ensure SHOW_DEBUG flag is used everywhere.
258 |
259 | `NEW`: Added German Translation. Thank you to @The-Deep-Sea for doing this.
260 |
261 | ## Version 2.0.16
262 |
263 | **Release Date**: August 9th, 2021
264 |
265 | Just back from vacation, I release what was is done now. There are more requests, which will be added in the following days.
266 |
267 | ### Changes in release 2.0.16
268 |
269 | `CHANGE`: Issue #60. Renamed the state for Rain to `heavy` from `heavy rain`.
270 |
271 | `FIX`: Visibility calculation not working.
272 |
273 | ## Version 2.0.15
274 |
275 | **Release Date**: August 9th, 2021
276 |
277 | Just back from vacation, I release what was is done now. There are more requests, which will be added in the following days.
278 |
279 | ### Changes in release 2.0.15
280 |
281 | `NEW`: **BREAKING CHANGE** 2 new Lightning sensors have been added, `lightning_strike_count_1hr` and `lightning_strike_count_3hr`. They represent the number of lightning strikes within the last hour and the last 3 hours. The 3 hour counter is in reality not new, as this was previously named `lightning_strike_count`, but has now been renamed. The `lightning_strike_count` now shows the number of lightning strikes in the last minute and can be used to give an indication of the severity of the thunderstorm.
282 |
283 | `FIX`: Issue #51. Delta_T value was wrong when using `imperial` units. The fix applied in 2.0.14 was not correct, but hopefully this works now.
284 |
285 | `NEW`: @crzykidd added a Docker Compose file, so if you are using Docker Compose, find the file `docker-compose.yml` and modify this with your own setup.
286 |
287 | `CHANGE`: @GlennGoddard fintuned the visibility calculation, so that sensor is now more accurate, taking more parameters in to account.
288 |
289 | ## Version 2.0.14
290 |
291 | **Release Date**: July 25th, 2021
292 |
293 | ### Changes in release 2.0.14
294 |
295 | `FIX`: Issue #44. A user reported a wrong value for the Forecast Condition icon. It cannot be reproduced, but this version adds better error handling, and logging of the value that causes the error, should it occur again.
296 |
297 | `FIX`: Issue #51. Delta_T value was wrong when using `imperial` units. Thanks to @GlennGoddard for spotting the issue, and suggesting the solution.
298 |
299 | ## Version 2.0.11
300 |
301 | **Release Date**: July 23rd, 2021
302 |
303 | ### Changes in release 2.0.11
304 |
305 | `FIX`: Visibility sensor caused a crash after 2.0.10, due to missing vars. This is now fixed.
306 |
307 | ## Version 2.0.10
308 |
309 | **Release Date**: July 20th, 2021
310 |
311 | ### Changes in release 2.0.10
312 |
313 | `CHANGE`: To support multi platform docker containers the new home for the container is on Docker Hub with the name **briis/weatherflow2mqtt**. This is where future upgrades will land. So please change your docker command to use this location. README file is updated with the location.
314 | So please change your docker command to use this location. README file is updated with the location.
315 | With this change, you should no longer have to build the container yourself if you run on a non-Intel HW platform like a Raspberry PI.
316 | I recommend you delete the current container and image, and then re-load it using the new location.
317 |
318 | `FIX`: Visibility sensor now takes in to account current weather conditions. Thanks to @GlennGoddard for making this change. Fixing issue #29
319 |
320 | ## Version 2.0.9
321 |
322 | **Release Date**: July 6th, 2021
323 |
324 | ### Changes in release 2.0.9
325 |
326 | `FIX`: Wetbulb Calculation crashed the system if one of the sensors had a NoneType value. Fixing issue #33
327 |
328 | `NEW`: Added French Tranlation, thanks to @titilambert.
329 |
330 | `FIX`: Issue #37, where the device status check could fail. thanks to @titilambert for fixing this.
331 |
332 | ## Version 2.0.8
333 |
334 | **Release Date**: June 24th, 2021
335 |
336 | ### Changes in release 2.0.8
337 |
338 | `FIX`: Wrong key in the Temperature Level description
339 |
340 | ## Version 2.0.7
341 |
342 | **Release Date**: June 24th, 2021
343 |
344 | ### Changes in release 2.0.7
345 |
346 | `NEW`: There is now multi language support for text states and other strings. Currently the support is limited to Danish (da) and English (en), and the default is English. In order to active another language than English add the following to the Docker Run command: `-e LANGUAGE=da`. If LANGUAGE is omitted, english will be used. So if this is the language you want, you don't have to do anything.
347 | If you want to help translate in to another language, go to Github and in the translations directory, download the `en.json` file, save it as `yourlanguagecode.json`, translate the strings, and make a pull request on Github.
348 | `FIX`: `sensor.wf_dewpoint_comfort_level` was not showing the correct value when using Imperial Units.
349 | `NEW`: A new sensor called `sensor.wf_beaufort_scale` is added. The Beaufort scale is an empirical measure that relates wind speed to observed conditions at sea or on land and holds a value between 0 and 12, where 0 is Calm and 12 is Hurricane force. The state holds the numeric value and there is an Attribute named `description` that holds the textual representation.
350 |
351 | ## Version 2.0.6
352 |
353 | **Release Date**: June 23rd, 2021
354 |
355 | ### Changes in release 2.0.6
356 |
357 | `FIX`: (Issue #28) The fix on release 2.0.5, was not completely solving the issue. Now a base Base value of Steady will be returned, if we are not able to calculate the Trend due to lack of data.
358 | `NEW`: (Issue #29) Adding new sensor `sensor.wf_dewpoint_comfort_level` which gives a textual representation of the Dewpoint value.
359 | `NEW`: (Issue #29) Adding new sensor `sensor.wf_temperature_level` which gives a textual representation of the Outside Air Temperature value.
360 | `NEW`: (Issue #29) Adding new sensor `sensor.wf_uv_level` which gives a textual representation of the UV Index value.
361 |
362 | ## Version 2.0.5
363 |
364 | **Release Date**: June 22nd, 2021
365 |
366 | ### Changes in release 2.0.5
367 |
368 | `FIX`: (Issue #28) Sometimes the Pressure Trend calculation would get the program to crash due a timing in when data was logged by the system. With this fix, a `None` value will be returned instead.
369 |
370 | ## Version 2.0.4
371 |
372 | **Release Date**: June 21st, 2021
373 |
374 | ### Changes in release 2.0.4
375 |
376 | `NEW`: A new sensor called `wf_wet_bulb_temperature` has been added. This sensor returns the temperature of a parcel of air cooled to saturation (100% relative humidity)
377 | `NEW`: A new sensor called `wf_delta_t` has been added. Delta T, is used in agriculture to indicate acceptable conditions for spraying pesticides and fertilizers. It is simply the difference between the air temperature (aka "dry bulb temperature") and the wet bulb temperature
378 | `NEW`: Added monthly min and max values to selected sensors. **Note** Data will only be updated once a day, so first values will be shown after midnight after the upgrade and new Attributes will require a restart of HA before they appear.
379 | `FIXED`: Daily Max value did not reset for some sensors at midnight.
380 | `FIXED`: When using the WeatherFlow forecast, there could be a mismatch in the condition state.
381 | `CHANGES`: Some *Under the Hood* changes to prepare for future enhancements.
382 |
383 | | Attribute Name | Description |
384 | | --- | --- |
385 | | `max_month` | Maximum value for the current month. Reset when new month. |
386 | | `max_month_time` | UTC time when the max value was recorded. Reset when new month. |
387 | | `min_month` | Minimum value for the current month. Reset when new month. |
388 | | `min_month_time` | UTC time when the min value was recorded. Reset when new month. |
389 |
390 | ## Version 2.0.3
391 |
392 | **Release Date**: June 18th, 2021
393 |
394 | ### Changes in release 2.0.3
395 |
396 | `NEW`: A new sensor called `wf_visibility`has been added. This sensor shows the distance to the horizon, in either km or nautical miles, depending on the unit_system.
397 |
398 | ## Version 2.0.2
399 |
400 | **Release Date**: June 18th, 2021
401 |
402 | ### Changes in release 2.0.2
403 |
404 | **Please make a backup of `weatherflow2mqtt.db` before upgrading. Just in case anything goes wrong.**
405 |
406 | `FIX`: If the forecast data from WeatherFlow is not available, the program will now just skip the update, and wait for the next timely update, instead of crashing the Container.
407 |
408 | `CHANGED`: Attributes for each sensors are now moved from the event topics, to each individual sensor, so that we can add sensor specific attributes. This will have no impact on a running system.
409 |
410 | `NEW`: Started the work on creating Sensors for High and Low values. A new table is created and daily high/low will be calculated and written to this table. Currently only day high and low plus all-time high and low values are calculated. The values are written as attributes to each individual sensor where I believe it is relevant to have these values. **Note** It takes 10 min before the daily max and min values are shown, and all-time values are first shown the following day after upgrading, or on the first run of this program.
411 |
412 | | Attribute Name | Description |
413 | | --- | --- |
414 | | `max_day` | Maximum value for the current day. Reset at midnight. |
415 | | `max_day_time` | UTC time when the max value was recorded. Reset at midnight. |
416 | | `min_day` | Minimum value for the current day. Reset at midnight. |
417 | | `min_day_time` | UTC time when the min value was recorded. Reset at midnight. |
418 | | `max_all` | Maximum value ever recorded. Updated at midnight every day. |
419 | | `max_all_time` | UTC time when the all-time max value was recorded. Updated at midnight every day. |
420 | | `min_all` | Minimum value ever recorded. Updated at midnight every day. |
421 | | `min_all_time` | UTC time when the all-time min value was recorded. Updated at midnight every day. |
422 |
423 | The following sensors are displaying Max and Min values:
424 |
425 | | Sensor ID | Max Value | Min Value |
426 | | --- | --- | --- |
427 | | `air_temperature` | Yes | Yes |
428 | | `dewpoint` | Yes | Yes |
429 | | `illuminance` | Yes | No |
430 | | `lightning_strike_count_today` | Yes | No |
431 | | `lightning_strike_energy` | Yes | No |
432 | | `rain_rate` | Yes | No |
433 | | `rain_duration_today` | Yes | No |
434 | | `relative_humidity` | Yes | Yes |
435 | | `sealevel_pressure` | Yes | Yes |
436 | | `solar_radiation` | Yes | No |
437 | | `uv` | Yes | No |
438 | | `wind_gust` | Yes | No |
439 | | `wind_lull` | Yes | No |
440 | | `wind_speed_avg` | Yes | No |
441 |
442 | ## Version 2.0.1
443 |
444 | **Release Date**: June 15th, 2021
445 |
446 | ### Changes in release 2.0.1
447 |
448 | * `FIX`: Fixing the AIR Density value, when using Imperial Metrics
449 |
450 | ## Version 2.0.0
451 |
452 | **Release Date**: June 15th, 2021
453 |
454 | ### Changes in release 2.0.0
455 |
456 | * `NEW`: There is now a new sensor called `pressure_trend`. This sensor monitors the Sea Level pressure changes. The Pressure Trend state is determined by the rate of change over the past 3 hours. It can be one of the following: `Steady`, `Falling` and `Rising`. Please note we will need to gather 3 hours of data, before the returned value will be correct. Until then the value will be `Steady`.
457 | * `NEW`: The simple storage system, that used two flat files, has now been rewritten, to use a SQLite Database instead. This will make future developments easier, and is also the foundation for the new Pressure Trend sensor. If you are already up and running with this program, your old data will automatically be migrated in to the new SQLite Database. And once you are confident that all is running, you can safely delete `.storage.json` and `.lightning.data`.
458 | * `BREAKING CHANGE`: To better seperate the sensors created by this Integration from other Weather related sensors, this version now prefixes all sensor names with `wf_` and all Friendly Names with `WF`. As each sensor has a Unique ID that does not change, the sensors will keep the old Entity Id, and just change the name, and only the Friendly Name will change after this upgrade. But if you delete the Integration, and re-add it, then all the sensors will have the `wf_` as a prefix. The same goes for new sensors that might be added in the future. So if you want to avoid any future issues, I recommend deleting the `WeatherFlow2MQTT` device from the MQTT Integration, and then restart the Docker Container, to get all the sensors added again, with the new naming convention.
459 |
--------------------------------------------------------------------------------
/DOCS.md:
--------------------------------------------------------------------------------
1 | # Home Assistant Community Add-on: WeatherFlow2MQTT
2 |
3 | This add-on allows you to get data from a WeatherFlow weather station using UDP. There is support for both the new Tempest station and the older AIR & SKY station.
4 |
5 | **Important**: this add-on uses the same timezone and unit system as your Home Assistant instance, so make sure it has been properly set.
6 |
7 | ## Installation
8 |
9 | To install the add-on, first follow the installation steps from the [README on GitHub](https://github.com/briis/hass-weatherflow2mqtt/blob/main/README.md).
10 |
11 | ## Configuration
12 |
13 | ### Option: `ELEVATION`: (default: Home Assistant Elevation)
14 |
15 | Set the height above sea level for where the station is placed. This is used when calculating some of the sensor values. Station elevation plus Device height above ground. The value has to be in meters (`meters = feet * 0.3048`). Default is _Home Assistant Elevation_
16 |
17 | ### Option: `LATITUDE`: (default: Home Assistant Latitude)
18 |
19 | Set the latitude for where the station is placed. This is used when calculating some of the sensor values. Default is _Home Assistant Latitude_
20 |
21 | ### Option: `LONGITUDE`: (default: Home Assistant Longitude)
22 |
23 | Set the longitude for where the station is placed. This is used when calculating some of the sensor values. Default is _Home Assistant Longitude_
24 |
25 | ### Option: `RAPID_WIND_INTERVAL`: (default: 0)
26 |
27 | The weather stations delivers wind speed and bearing every 2 seconds. If you don't want to update the HA sensors so often, you can set a number here (in seconds), for how often they are updated. Default is _0_, which means data are updated when received from the station.
28 |
29 | ### Option: `STATION_ID`: (default: None)
30 |
31 | Enter your Station ID for your WeatherFlow Station.
32 |
33 | ### Option: `STATION_TOKEN`: (default: None)
34 |
35 | Enter your personal access Token to allow retrieval of data. If you don't have the token [login with your account](https://tempestwx.com/settings/tokens) and create the token. **NOTE** You must own a WeatherFlow station to get this token.
36 |
37 | ### Option: `FORECAST_INTERVAL`: (default: 30)
38 |
39 | The interval in minutes, between updates of the Forecast data.
40 |
41 | ### Option: `LANGUAGE`: (default: en)
42 |
43 | Use this to set the language for Wind Direction cardinals and other sensors with text strings as state value. These strings will then be displayed in HA in the selected language.
44 |
45 | ### Option: `FILTER_SENSORS`: (default: None)
46 |
47 | A comma-separated list of sensors to include instead of loading all sensors. Default is _None_, which disables filtering such that all sensors are loaded.
48 |
49 | ### Option: `INVERT_FILTER`: (default: False)
50 |
51 | If set to True, `FILTER_SENSORS` will be treated as an exclusion list such that the specified sensors are ignored. Default is _False_.
52 |
53 | ### Option: `MQTT_HOST`: (default: Installed MQTT Add-On IP)
54 |
55 | The IP address of your mqtt server. Even though you have the MQTT Server on the same machine as this Container, don't use `127.0.0.1` as this will resolve to an IP Address inside your container. Use the external IP Address.
56 |
57 | ### Option: `MQTT_PORT`: (default: 1883)
58 |
59 | The Port for your mqtt server. Default value is _1883_
60 |
61 | ### Option: `MQTT_USERNAME`: (default: Installed MQTT Add-On username)
62 |
63 | The username used to connect to the mqtt server. Leave blank to use Anonymous connection.
64 |
65 | ### Option: `MQTT_PASSWORD`: (default: Installed MQTT Add-On password)
66 |
67 | The password used to connect to the mqtt server. Leave blank to use Anonymous connection.
68 |
69 | ### Option: `MQTT_DEBUG`: (default: False)
70 |
71 | Set this to True, to get some more mqtt debugging messages in the Container log file.
72 |
73 | ### Option: `WF_HOST`: (default: 0.0.0.0)
74 |
75 | Unless you have a very special IP setup or the Weatherflow hub is on a different network, you should not change this. Default is _0.0.0.0_
76 |
77 | ### Option: `WF_PORT`: (default: 50222)
78 |
79 | Weatherflow always broadcasts on port 50222/udp, so don't change this. Default is _50222_
80 |
81 | ### Option: `DEBUG`: (default: False)
82 |
83 | Set this to True to enable more debug data in the Container Log.
84 |
85 | ## Troubleshooting
86 |
87 | ### VLANs and Subnets
88 | WeatherFlow2MQTT will not discover your base station if it is on a separate VLAN/subnet from your Home Assistant/Docker instance. Common indications that you are on a different VLAN are error messages such as:
89 | ```weatherflow2mqtt.weatherflow_mqtt:Could not start listening to the UDP Socket. Error is: Could not open a local UDP endpoint```
90 |
91 | Although WeatherFlow2MQTT allows you to manually specify the Weatherflow host, there will still be issues getting the UDP broadcast to travel across the VLAN.
92 |
93 | ## Authors & contributors
94 |
95 | The original setup of this repository is by [Bjarne Riis](https://github.com/briis).
96 |
97 | For a full list of all authors and contributors, check the [contributor's page](https://github.com/briis/hass-weatherflow2mqtt/graphs/contributors).
98 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11-slim-buster
2 | LABEL org.opencontainers.image.source="https://github.com/briis/hass-weatherflow2mqtt"
3 |
4 | RUN mkdir -p /data
5 | WORKDIR /src/weatherflow2mqtt
6 |
7 | ADD requirements.txt test_requirements.txt /src/weatherflow2mqtt/
8 | ADD weatherflow2mqtt /src/weatherflow2mqtt/weatherflow2mqtt/
9 | ADD setup.py /src/weatherflow2mqtt/
10 |
11 | RUN apt-get update \
12 | && apt-get -y install build-essential \
13 | && pip install --upgrade --no-cache-dir pip \
14 | && pip install --no-cache-dir -r requirements.txt \
15 | && python setup.py install \
16 | && apt-get purge -y --auto-remove build-essential \
17 | && rm -rf /var/lib/apt/lists/*
18 |
19 |
20 | ENV TZ=Europe/Copenhagen
21 |
22 | EXPOSE 50222/udp
23 | EXPOSE 1883
24 |
25 | CMD [ "weatherflow2mqtt" ]
26 |
--------------------------------------------------------------------------------
/LATEST_CHANGES.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [3.2.2] - 2023-10-08
6 |
7 | ### BREAKING Announcement
8 |
9 | As there is now a `Home Assistant Core` integration for WeatherFlow which uses the UDP API, I had to make a [new Integration](https://github.com/briis/weatherflow_forecast) that uses the REST API, with a different name (WeatherFlow Forecast). The new integration is up-to-date with the latest specs for how to create a Weather Forecast, and also gives the option to only add the Forecast, and no additional sensors.
10 |
11 | There is no *Weather Entity* in Home Assistant for MQTT, so after attributes are deprecated in Home Assistant 2024.3, there is no option to add the Forecast to Home Assistant.
12 | As a consequence of that, I have decided to remove the ability for this Add-On to add Forecast data to MQTT and Home Assistant. This Add-On will still be maintained, but just without the option of a Forecast - meaning it will be 100% local.
13 | If you want the forecast in combination with this Add-On, install the new integration mentioned above, just leave the *Add sensors* box unchecked.
14 |
15 | There is not an exact date for when this will happen, but it will be before end of February 2024.
16 |
17 | ### Changes
18 |
19 | - Added Slovenian language file. This was unfortunately placed in a wrong directory and as such it was not read by the integration. Fixing issue #236
20 | - Fixed issue #244 with deprecated forecast values. Thank you @mjmeli
21 | - Corrected visibility imperial unit from nautical mile (nmi) to mile (mi)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Bjarne Riis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "WeatherFlow to MQTT",
3 | "description": "WeatherFlow to MQTT for Home Assistant",
4 | "slug": "weatherflow2mqtt",
5 | "url": "https://github.com/briis/hass-weatherflow2mqtt",
6 | "image": "briis/weatherflow2mqtt",
7 | "version": "3.2.2",
8 | "arch": [
9 | "armv7",
10 | "aarch64",
11 | "amd64"
12 | ],
13 | "startup": "application",
14 | "boot": "auto",
15 | "ports": {
16 | "50222/udp": 50222
17 | },
18 | "ports_description": {
19 | "50222/udp": "WeatherFlow socket"
20 | },
21 | "environment": {
22 | "HA_SUPERVISOR": "True"
23 | },
24 | "homeassistant_api": true,
25 | "services": [
26 | "mqtt:want"
27 | ],
28 | "options": {},
29 | "schema": {
30 | "ELEVATION": "float?",
31 | "LATITUDE": "float?",
32 | "LONGITUDE": "float?",
33 | "RAPID_WIND_INTERVAL": "int?",
34 | "STATION_ID": "str?",
35 | "STATION_TOKEN": "str?",
36 | "FORECAST_INTERVAL": "int?",
37 | "LANGUAGE": "list(en|da|de|fr|nl|se)?",
38 | "FILTER_SENSORS": "str?",
39 | "INVERT_FILTER": "bool?",
40 | "MQTT_HOST": "str?",
41 | "MQTT_PORT": "port?",
42 | "MQTT_USERNAME": "str?",
43 | "MQTT_PASSWORD": "password?",
44 | "MQTT_DEBUG": "bool?",
45 | "WF_HOST": "str?",
46 | "WF_PORT": "port?",
47 | "DEBUG": "bool?",
48 | "ZAMBRETTI_MIN_PRESSURE": "float?",
49 | "ZAMBRETTI_MAX_PRESSURE": "float?"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/config_example_AIR_SKY.yaml:
--------------------------------------------------------------------------------
1 | sensors:
2 | - absolute_humidity
3 | - air_temperature
4 | - battery
5 | - battery_level
6 | - beaufort
7 | - dewpoint
8 | - dewpoint_description
9 | - delta_t
10 | - feelslike
11 | - illuminance
12 | - lightning_strike_count
13 | - lightning_strike_count_1hr
14 | - lightning_strike_count_3hr
15 | - lightning_strike_count_today
16 | - lightning_strike_distance
17 | - lightning_strike_energy
18 | - lightning_strike_time
19 | - precipitation_type
20 | - pressure_trend
21 | - rain_intensity
22 | - rain_rate
23 | - rain_start_time
24 | - rain_today
25 | - rain_yesterday
26 | - rain_duration_today
27 | - rain_duration_yesterday
28 | - relative_humidity
29 | - sealevel_pressure
30 | - sky_status
31 | - solar_elevation
32 | - solar_insolation
33 | - solar_radiation
34 | - station_pressure
35 | - status
36 | - temperature_description
37 | - uv
38 | - uv_description
39 | - visibility
40 | - wetbulb
41 | - wbgt #wet_bulb_globe_temperature
42 | - wind_bearing
43 | - wind_bearing_avg
44 | - wind_direction
45 | - wind_direction_avg
46 | - wind_gust
47 | - wind_lull
48 | - wind_speed
49 | - wind_speed_avg
50 | - weather # Only if you are loading the Forecast
51 |
--------------------------------------------------------------------------------
/config_example_TEMPEST.yaml:
--------------------------------------------------------------------------------
1 | sensors:
2 | - absolute_humidity
3 | - air_temperature
4 | - battery
5 | - battery_level
6 | - battery_mode
7 | - beaufort
8 | - dewpoint
9 | - dewpoint_description
10 | - delta_t
11 | - feelslike
12 | - illuminance
13 | - lightning_strike_count
14 | - lightning_strike_count_1hr
15 | - lightning_strike_count_3hr
16 | - lightning_strike_count_today
17 | - lightning_strike_distance
18 | - lightning_strike_energy
19 | - lightning_strike_time
20 | - precipitation_type
21 | - pressure_trend
22 | - rain_intensity
23 | - rain_rate
24 | - rain_start_time
25 | - rain_today
26 | - rain_yesterday
27 | - rain_duration_today
28 | - rain_duration_yesterday
29 | - relative_humidity
30 | - sealevel_pressure
31 | - solar_elevation
32 | - solar_insolation
33 | - solar_radiation
34 | - station_pressure
35 | - temperature_description
36 | - tempest_status
37 | - uv
38 | - uv_description
39 | - visibility
40 | - wetbulb
41 | - wbgt #wet_bulb_globe_temperature
42 | - wind_bearing
43 | - wind_bearing_avg
44 | - wind_direction
45 | - wind_direction_avg
46 | - wind_gust
47 | - wind_lull
48 | - wind_speed
49 | - wind_speed_avg
50 | - weather # Only if you are loading the Forecast
51 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | services:
3 | weatherflow2mqtt:
4 | image: briis/weatherflow2mqtt:latest
5 | restart: unless-stopped
6 | environment:
7 | - TZ=America/Los_Angeles
8 | - UNIT_SYSTEM=imperial
9 | - LANGUAGE=en
10 | - RAPID_WIND_INTERVAL=0
11 | - DEBUG=False
12 | - ELEVATION=0
13 | - LATITUDE=00.0000
14 | - LONGITUDE=000.0000
15 | - ZAMBRETTI_MIN_PRESSURE=
16 | - ZAMBRETTI_MAX_PRESSURE=
17 | - WF_HOST=0.0.0.0
18 | - WF_PORT=50222
19 | - MQTT_HOST=
20 | - MQTT_PORT=1883
21 | - MQTT_USERNAME=
22 | - MQTT_PASSWORD=
23 | - MQTT_DEBUG=False
24 | - STATION_ID=
25 | - STATION_TOKEN=
26 | - FORECAST_INTERVAL=30
27 | volumes:
28 | - /YOUR_STORAGE_AREA/PATH:/data
29 | ports:
30 | - 0.0.0.0:50222:50222/udp
31 |
--------------------------------------------------------------------------------
/hass-weatherflow2mqtt.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ],
7 | "settings": {
8 | "python.pythonPath": "/usr/local/bin/python3"
9 | }
10 | }
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briis/hass-weatherflow2mqtt/24cfd044480f272c8432728e7bbad7d0a66750a3/icon.png
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briis/hass-weatherflow2mqtt/24cfd044480f272c8432728e7bbad7d0a66750a3/logo.png
--------------------------------------------------------------------------------
/repository.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "WeatherFlow to MQTT",
3 | "url": "https://github.com/briis/hass-weatherflow2mqtt"
4 | }
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | paho-mqtt==1.6.1
2 | aiohttp==3.8.4
3 | PyYAML==6.0.1
4 | pytz==2023.3
5 | pyweatherflowudp==1.4.2
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from setuptools import setup
4 | from weatherflow2mqtt.__version__ import VERSION
5 |
6 | if sys.version_info < (3,10):
7 | sys.exit('Sorry, Python < 3.10 is not supported')
8 |
9 | install_requires = list(val.strip() for val in open('requirements.txt'))
10 | tests_require = list(val.strip() for val in open('test_requirements.txt'))
11 |
12 | setup(name='weatherflow2mqtt',
13 | version=VERSION,
14 | description=('WeatherFlow-2-MQTT for Home Assistant'),
15 | author='Bjarne Riis',
16 | author_email='bjarne@briis.com',
17 | url='https://github.com/briis/hass-weatherflow2mqtt',
18 | package_data={
19 | '': ['LICENSE.txt'],
20 | 'weatherflow2mqtt': ['translations/*.json'],
21 | },
22 | include_package_data=True,
23 | packages=['weatherflow2mqtt'],
24 | entry_points={
25 | 'console_scripts': [
26 | 'weatherflow2mqtt = weatherflow2mqtt.__main__:main'
27 | ]
28 | },
29 | license='MIT',
30 | install_requires=install_requires,
31 | tests_require=tests_require,
32 | classifiers=[
33 | 'Programming Language :: Python :: 3.10',
34 | ]
35 |
36 | )
37 |
--------------------------------------------------------------------------------
/test_requirements.txt:
--------------------------------------------------------------------------------
1 | flake8
2 | pycodestyle==2.5.0
3 | pydocstyle
4 | pylint
5 | pytest==4.4.1
6 | pytest-cov==2.6.1
7 | pytest-timeout==1.3.3
8 | #Sphinx==1.8.4
9 | #sphinx-rtd-theme==0.4.3
10 | #tox==3.9.0
11 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E226,E302,E41, W503
3 | max-line-length = 100
4 | exclude = tests/*
5 | max-complexity = 10
6 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/__init__.py:
--------------------------------------------------------------------------------
1 | """A Program to receive UDP data from Weatherflow and Publish to MQTT."""
2 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/__main__.py:
--------------------------------------------------------------------------------
1 | """Main module."""
2 | import asyncio
3 |
4 | from weatherflow2mqtt import weatherflow_mqtt
5 |
6 |
7 | def main():
8 | """Start Main Program."""
9 | try:
10 | asyncio.run(weatherflow_mqtt.main())
11 | except KeyboardInterrupt:
12 | print("\nExiting Program")
13 |
14 |
15 | if __name__ == "__main__":
16 | main()
17 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/__version__.py:
--------------------------------------------------------------------------------
1 | """Module defining version."""
2 | VERSION = "3.2.2"
3 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/const.py:
--------------------------------------------------------------------------------
1 | """Constant file for weatherflow2mqtt."""
2 | import datetime
3 | import os
4 |
5 | ATTRIBUTION = "Powered by WeatherFlow2MQTT"
6 | DOMAIN = "weatherflow2mqtt"
7 | MANUFACTURER = "WeatherFlow"
8 |
9 | ATTR_ATTRIBUTION = "attribution"
10 | ATTR_FORECAST_CONDITION = "condition"
11 | ATTR_FORECAST_PRECIPITATION = "precipitation"
12 | ATTR_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability"
13 | ATTR_FORECAST_PRESSURE = "pressure"
14 | ATTR_FORECAST_TEMP = "temperature"
15 | ATTR_FORECAST_TEMP_LOW = "templow"
16 | ATTR_FORECAST_TIME = "datetime"
17 | ATTR_FORECAST_WIND_BEARING = "wind_bearing"
18 | ATTR_FORECAST_WIND_SPEED = "wind_speed"
19 | ATTR_FORECAST_HUMIDITY = "humidity"
20 |
21 | EXTERNAL_DIRECTORY = os.environ.get("EXTERNAL_DIRECTORY", "/data")
22 | INTERNAL_DIRECTORY = "/app"
23 | STORAGE_FILE = f"{EXTERNAL_DIRECTORY}/.storage.json"
24 | DATABASE = f"{EXTERNAL_DIRECTORY}/weatherflow2mqtt.db"
25 | DATABASE_VERSION = 2
26 | STORAGE_ID = 1
27 |
28 | TABLE_STORAGE = """ CREATE TABLE IF NOT EXISTS storage (
29 | id integer PRIMARY KEY,
30 | rain_today real,
31 | rain_yesterday real,
32 | rain_start real,
33 | rain_duration_today integer,
34 | rain_duration_yesterday integer,
35 | lightning_count integer,
36 | lightning_count_today integer,
37 | last_lightning_time real,
38 | last_lightning_distance integer,
39 | last_lightning_energy
40 | );"""
41 |
42 | TABLE_PRESSURE = """ CREATE TABLE IF NOT EXISTS pressure (
43 | timestamp real PRIMARY KEY,
44 | pressure real
45 | );"""
46 |
47 | TABLE_LIGHTNING = """ CREATE TABLE IF NOT EXISTS lightning (
48 | timestamp real PRIMARY KEY
49 | );"""
50 |
51 | TABLE_HIGH_LOW = """
52 | CREATE TABLE IF NOT EXISTS high_low (
53 | sensorid TEXT PRIMARY KEY,
54 | latest REAL,
55 | max_day REAL,
56 | max_day_time REAL,
57 | min_day REAL,
58 | min_day_time REAL,
59 | max_yday REAL,
60 | max_yday_time REAL,
61 | min_yday REAL,
62 | min_yday_time REAL,
63 | max_week REAL,
64 | max_week_time REAL,
65 | min_week REAL,
66 | min_week_time REAL,
67 | max_month REAL,
68 | max_month_time REAL,
69 | min_month REAL,
70 | min_month_time REAL,
71 | max_year REAL,
72 | max_year_time REAL,
73 | min_year REAL,
74 | min_year_time REAL,
75 | max_all REAL,
76 | max_all_time REAL,
77 | min_all REAL,
78 | min_all_time REAL
79 | );
80 | """
81 |
82 | TABLE_DAY_DATA = """ CREATE TABLE IF NOT EXISTS day_data (
83 | timestamp REAL PRIMARY KEY,
84 | air_temperature REAL,
85 | relative_humidity REAL,
86 | dewpoint REAL,
87 | illuminance REAL,
88 | rain_duration_today REAL,
89 | rain_rate REAL,
90 | wind_gust REAL,
91 | wind_lull REAL,
92 | wind_speed_avg REAL,
93 | lightning_strike_energy REAL,
94 | lightning_strike_count_today REAL,
95 | sealevel_pressure REAL,
96 | uv REAL,
97 | solar_radiation REAL
98 | );"""
99 |
100 | COL_TEMPERATURE = "air_temperature"
101 | COL_HUMIDITY = "relative_humidity"
102 | COL_DEWPOINT = "dewpoint"
103 | COL_ILLUMINANCE = "illuminance"
104 | COL_RAINDURATION = "rain_duration_today"
105 | COL_RAINRATE = "rain_rate"
106 | COL_WINDGUST = "wind_gust"
107 | COL_WINDLULL = "wind_lull"
108 | COL_WINDSPEED = "wind_speed_avg"
109 | COL_STRIKEENERGY = "lightning_strike_energy"
110 | COL_STRIKECOUNT = "lightning_strike_count_today"
111 | COL_PRESSURE = "sealevel_pressure"
112 | COL_UV = "uv"
113 | COL_SOLARRAD = "solar_radiation"
114 |
115 | BASE_URL = "https://swd.weatherflow.com/swd/rest"
116 |
117 | BATTERY_MODE_DESCRIPTION = [
118 | "All sensors enabled and operating at full performance. Wind sampling interval every 3 seconds",
119 | "Wind sampling interval set to 6 seconds",
120 | "Wind sampling interval set to one minute",
121 | "Wind sampling interval set to 5 minutes. All other sensors sampling interval set to 5 minutes. Haptic Rain sensor disabled from active listening",
122 | ]
123 |
124 | DEFAULT_TIMEOUT = 10
125 |
126 | DEVICE_CLASS_BATTERY = "battery"
127 | DEVICE_CLASS_DISTANCE = "distance"
128 | DEVICE_CLASS_HUMIDITY = "humidity"
129 | DEVICE_CLASS_ILLUMINANCE = "illuminance"
130 | DEVICE_CLASS_IRRADIANCE = "irradiance"
131 | DEVICE_CLASS_PRECIPITATION="precipitation"
132 | DEVICE_CLASS_PRECIPITATION_INTENSITY="precipitation_intensity"
133 | DEVICE_CLASS_PRESSURE = "pressure"
134 | DEVICE_CLASS_TEMPERATURE = "temperature"
135 | DEVICE_CLASS_TIMESTAMP = "timestamp"
136 | DEVICE_CLASS_VOLTAGE = "voltage"
137 | DEVICE_CLASS_WIND_SPEED="wind_speed"
138 |
139 |
140 | STATE_CLASS_MEASUREMENT = "measurement"
141 | STATE_CLASS_INCREASING = "total_increasing"
142 |
143 | EVENT_FORECAST = "weather"
144 | EVENT_HIGH_LOW = "high_low"
145 |
146 | FORECAST_TYPE_DAILY = "daily"
147 | FORECAST_TYPE_HOURLY = "hourly"
148 | FORECAST_ENTITY = "weather"
149 | FORECAST_HOURLY_HOURS = 36
150 |
151 | STRIKE_COUNT_TIMER = 3 * 60 * 60
152 | PRESSURE_TREND_TIMER = 3 * 60 * 60
153 | HIGH_LOW_TIMER = 10 * 60
154 |
155 | LANGUAGE_ENGLISH = "en"
156 | LANGUAGE_DANISH = "da"
157 | LANGUAGE_GERMAN = "de"
158 | LANGUAGE_FRENCH = "fr"
159 | LANGUAGE_DUTCH = "nl"
160 | LANGUAGE_SLOVENIA = "si"
161 | SUPPORTED_LANGUAGES = [
162 | LANGUAGE_ENGLISH,
163 | LANGUAGE_DANISH,
164 | LANGUAGE_GERMAN,
165 | LANGUAGE_FRENCH,
166 | LANGUAGE_DUTCH,
167 | LANGUAGE_SLOVENIA,
168 | ]
169 |
170 | TEMP_CELSIUS = "°C"
171 | TEMP_FAHRENHEIT = "°F"
172 | UNITS_IMPERIAL = "imperial"
173 | UNITS_METRIC = "metric"
174 |
175 | UTC = datetime.timezone.utc
176 |
177 | ZAMBRETTI_MIN_PRESSURE = 960
178 | ZAMBRETTI_MAX_PRESSURE = 1060
179 |
180 | CONDITION_CLASSES = {
181 | "clear-night": ["clear-night"],
182 | "cloudy": ["cloudy"],
183 | "exceptional": ["cloudy"],
184 | "fog": ["foggy"],
185 | "hail": ["hail"],
186 | "lightning": ["thunderstorm"],
187 | "lightning-rainy": ["possibly-thunderstorm-day", "possibly-thunderstorm-night"],
188 | "partlycloudy": [
189 | "partly-cloudy-day",
190 | "partly-cloudy-night",
191 | ],
192 | "rainy": [
193 | "rainy",
194 | "possibly-rainy-day",
195 | "possibly-rainy-night",
196 | ],
197 | "snowy": ["snow", "possibly-snow-day", "possibly-snow-night"],
198 | "snowy-rainy": ["sleet", "possibly-sleet-day", "possibly-sleet-night"],
199 | "sunny": ["clear-day"],
200 | "windy": ["windy"],
201 | }
202 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/forecast.py:
--------------------------------------------------------------------------------
1 | """Module to get forecast using REST from WeatherFlow."""
2 | from __future__ import annotations
3 |
4 | import asyncio
5 | import logging
6 | from dataclasses import dataclass
7 | from datetime import datetime
8 | from typing import Any, OrderedDict
9 |
10 | from aiohttp import ClientSession, ClientTimeout
11 | from aiohttp.client_exceptions import ClientError
12 |
13 | from .const import (
14 | ATTR_ATTRIBUTION,
15 | ATTR_FORECAST_CONDITION,
16 | ATTR_FORECAST_HUMIDITY,
17 | ATTR_FORECAST_PRECIPITATION,
18 | ATTR_FORECAST_PRECIPITATION_PROBABILITY,
19 | ATTR_FORECAST_PRESSURE,
20 | ATTR_FORECAST_TEMP,
21 | ATTR_FORECAST_TEMP_LOW,
22 | ATTR_FORECAST_TIME,
23 | ATTR_FORECAST_WIND_BEARING,
24 | ATTR_FORECAST_WIND_SPEED,
25 | ATTRIBUTION,
26 | BASE_URL,
27 | CONDITION_CLASSES,
28 | DEFAULT_TIMEOUT,
29 | FORECAST_HOURLY_HOURS,
30 | FORECAST_TYPE_DAILY,
31 | FORECAST_TYPE_HOURLY,
32 | LANGUAGE_ENGLISH,
33 | UNITS_METRIC,
34 | )
35 | from .helpers import ConversionFunctions
36 |
37 | _LOGGER = logging.getLogger(__name__)
38 |
39 |
40 | @dataclass
41 | class ForecastConfig:
42 | """Forecast config."""
43 |
44 | station_id: str
45 | token: str
46 | interval: int = 30
47 |
48 |
49 | class Forecast:
50 | """Forecast."""
51 |
52 | def __init__(
53 | self,
54 | station_id: str,
55 | token: str,
56 | interval: int = 30,
57 | conversions: ConversionFunctions = ConversionFunctions(
58 | unit_system=UNITS_METRIC, language=LANGUAGE_ENGLISH
59 | ),
60 | session: ClientSession | None = None,
61 | ):
62 | """Initialize a Forecast object."""
63 | self.station_id = station_id
64 | self.token = token
65 | self.interval = interval
66 | self.conversions = conversions
67 | self._session: ClientSession = session
68 |
69 | @classmethod
70 | def from_config(
71 | cls,
72 | config: ForecastConfig,
73 | conversions: ConversionFunctions = ConversionFunctions(
74 | unit_system=UNITS_METRIC, language=LANGUAGE_ENGLISH
75 | ),
76 | session: ClientSession | None = None,
77 | ) -> Forecast:
78 | """Create a Forecast from a Forecast Config."""
79 | return cls(
80 | station_id=config.station_id,
81 | token=config.token,
82 | interval=config.interval,
83 | conversions=conversions,
84 | session=session,
85 | )
86 |
87 | async def update_forecast(self):
88 | """Return the formatted forecast data."""
89 | json_data = await self.async_request(
90 | method="get",
91 | endpoint=f"better_forecast?station_id={self.station_id}&token={self.token}",
92 | )
93 | items = []
94 |
95 | if json_data is not None:
96 | # We need a few Items from the Current Conditions section
97 | current_cond = json_data.get("current_conditions")
98 | current_icon = current_cond["icon"]
99 | today = datetime.date(datetime.now())
100 |
101 | # Prepare for MQTT
102 | condition_data = OrderedDict()
103 | condition_state = self.ha_condition_value(current_icon)
104 | condition_data["weather"] = condition_state
105 |
106 | forecast_data = json_data.get("forecast")
107 |
108 | # We also need Day hign and low Temp from Today
109 | temp_high_today = self.conversions.temperature(
110 | forecast_data[FORECAST_TYPE_DAILY][0]["air_temp_high"]
111 | )
112 | temp_low_today = self.conversions.temperature(
113 | forecast_data[FORECAST_TYPE_DAILY][0]["air_temp_low"]
114 | )
115 |
116 | # Process Daily Forecast
117 | fcst_data = OrderedDict()
118 | fcst_data[ATTR_ATTRIBUTION] = ATTRIBUTION
119 | fcst_data["temp_high_today"] = temp_high_today
120 | fcst_data["temp_low_today"] = temp_low_today
121 |
122 | for row in forecast_data[FORECAST_TYPE_DAILY]:
123 | # Skip over past forecasts - seems the API sometimes returns old forecasts
124 | forecast_time = datetime.date(
125 | datetime.fromtimestamp(row["day_start_local"])
126 | )
127 | if today > forecast_time:
128 | continue
129 |
130 | # Calculate data from hourly that's not summed up in the daily.
131 | precip = 0
132 | wind_avg = []
133 | wind_bearing = []
134 | for hourly in forecast_data["hourly"]:
135 | if hourly["local_day"] == row["day_num"]:
136 | precip += hourly["precip"]
137 | wind_avg.append(hourly["wind_avg"])
138 | wind_bearing.append(hourly["wind_direction"])
139 | sum_wind_avg = sum(wind_avg) / len(wind_avg)
140 | sum_wind_bearing = sum(wind_bearing) / len(wind_bearing) % 360
141 |
142 | item = {
143 | ATTR_FORECAST_TIME: self.conversions.utc_from_timestamp(
144 | row["day_start_local"]
145 | ),
146 | ATTR_FORECAST_CONDITION: "cloudy" if row.get("icon") is None else self.ha_condition_value(row["icon"]),
147 | ATTR_FORECAST_TEMP: self.conversions.temperature(
148 | row["air_temp_high"]
149 | ),
150 | ATTR_FORECAST_TEMP_LOW: self.conversions.temperature(
151 | row["air_temp_low"]
152 | ),
153 | ATTR_FORECAST_PRECIPITATION: self.conversions.rain(precip),
154 | ATTR_FORECAST_PRECIPITATION_PROBABILITY: row["precip_probability"],
155 | ATTR_FORECAST_WIND_SPEED: self.conversions.speed(
156 | sum_wind_avg, True
157 | ),
158 | ATTR_FORECAST_WIND_BEARING: int(sum_wind_bearing),
159 | }
160 | items.append(item)
161 | fcst_data["daily_forecast"] = items
162 |
163 | cnt = 0
164 | items = []
165 | for row in forecast_data[FORECAST_TYPE_HOURLY]:
166 | # Skip over past forecasts - seems the API sometimes returns old forecasts
167 | forecast_time = datetime.fromtimestamp(row["time"])
168 | if datetime.now() > forecast_time:
169 | continue
170 |
171 | item = {
172 | ATTR_FORECAST_TIME: self.conversions.utc_from_timestamp(
173 | row["time"]
174 | ),
175 | ATTR_FORECAST_CONDITION: self.ha_condition_value(row.get("icon")),
176 | ATTR_FORECAST_TEMP: self.conversions.temperature(
177 | row["air_temperature"]
178 | ),
179 | ATTR_FORECAST_PRESSURE: self.conversions.pressure(
180 | row.get("sea_level_pressure", 0)
181 | ),
182 | ATTR_FORECAST_HUMIDITY: row["relative_humidity"],
183 | ATTR_FORECAST_PRECIPITATION: self.conversions.rain(row["precip"]),
184 | ATTR_FORECAST_PRECIPITATION_PROBABILITY: row["precip_probability"],
185 | ATTR_FORECAST_WIND_SPEED: self.conversions.speed(
186 | row["wind_avg"], True
187 | ),
188 | ATTR_FORECAST_WIND_BEARING: row["wind_direction"],
189 | }
190 | items.append(item)
191 | # Limit number of Hours
192 | cnt += 1
193 | if cnt >= FORECAST_HOURLY_HOURS:
194 | break
195 | fcst_data["hourly_forecast"] = items
196 |
197 | return condition_data, fcst_data
198 |
199 | # Return None if we could not retrieve data
200 | _LOGGER.warning("Forecast Server was unresponsive. Skipping forecast update")
201 | return None, None
202 |
203 | async def async_request(self, method: str, endpoint: str) -> dict[str, Any]:
204 | """Request data from the WeatherFlow API."""
205 | use_running_session = self._session and not self._session.closed
206 |
207 | if use_running_session:
208 | session = self._session
209 | else:
210 | session = ClientSession(timeout=ClientTimeout(total=DEFAULT_TIMEOUT))
211 |
212 | try:
213 | async with session.request(method, f"{BASE_URL}/{endpoint}") as resp:
214 | resp.raise_for_status()
215 | data = await resp.json()
216 | return data
217 | except asyncio.TimeoutError:
218 | _LOGGER.debug("Request to endpoint timed out: %s", endpoint)
219 | except ClientError as err:
220 | if "Unauthorized" in str(err):
221 | _LOGGER.error(
222 | "Your API Key is invalid or does not support this operation"
223 | )
224 | if "Not Found" in str(err):
225 | _LOGGER.error("The Station ID does not exist")
226 | except Exception as exc:
227 | _LOGGER.debug("Error requesting data from %s Error: %s", endpoint, exc)
228 |
229 | finally:
230 | if not use_running_session:
231 | await session.close()
232 |
233 | def ha_condition_value(self, value: str) -> str | None:
234 | """Return Home Assistant Condition."""
235 | try:
236 | return next(
237 | (k for k, v in CONDITION_CLASSES.items() if value in v),
238 | None,
239 | )
240 | except Exception as exc:
241 | _LOGGER.debug(
242 | "Could not find icon with value: %s. Error message: %s", value, exc
243 | )
244 | return None
245 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/sensor_description.py:
--------------------------------------------------------------------------------
1 | """Sensor descriptions."""
2 | from __future__ import annotations
3 |
4 | from dataclasses import dataclass, field
5 | from typing import Any, Callable
6 |
7 | from pyweatherflowudp.device import (
8 | EVENT_OBSERVATION,
9 | EVENT_RAPID_WIND,
10 | EVENT_STATUS_UPDATE,
11 | TempestDevice,
12 | WeatherFlowDevice,
13 | )
14 |
15 | from weatherflow2mqtt.helpers import ConversionFunctions
16 | from weatherflow2mqtt.sqlite import SQLFunctions
17 |
18 | from .const import (
19 | DEVICE_CLASS_BATTERY,
20 | DEVICE_CLASS_DISTANCE,
21 | DEVICE_CLASS_HUMIDITY,
22 | DEVICE_CLASS_ILLUMINANCE,
23 | DEVICE_CLASS_IRRADIANCE,
24 | DEVICE_CLASS_PRECIPITATION,
25 | DEVICE_CLASS_PRECIPITATION_INTENSITY,
26 | DEVICE_CLASS_PRESSURE,
27 | DEVICE_CLASS_TEMPERATURE,
28 | DEVICE_CLASS_TIMESTAMP,
29 | DEVICE_CLASS_VOLTAGE,
30 | DEVICE_CLASS_WIND_SPEED,
31 | EVENT_FORECAST,
32 | FORECAST_ENTITY,
33 | STATE_CLASS_MEASUREMENT,
34 | TEMP_CELSIUS,
35 | TEMP_FAHRENHEIT,
36 | )
37 | from .helpers import NO_CONVERSION, no_conversion_to_none
38 |
39 | ALTITUDE_FEET = "ft"
40 | ALTITUDE_METERS = "m"
41 | UV_INDEX = "UV index"
42 |
43 |
44 | @dataclass
45 | class BaseSensorDescription:
46 | """Base sensor description."""
47 |
48 | id: str
49 | name: str
50 | event: str
51 |
52 | attr: str | None = None
53 | device_class: str | None = None
54 | extra_att: bool = False
55 | has_description: bool = False
56 | icon: str | None = None
57 | last_reset: bool = False
58 | show_min_att: bool = False
59 | state_class: str | None = None
60 | unit_i: str | None = None
61 | unit_i_cnv: str | None = None
62 | unit_m: str | None = None
63 | unit_m_cnv: str | None = None
64 |
65 | @property
66 | def device_attr(self) -> str:
67 | """Return the device attr."""
68 | return self.id if self.attr is None else self.attr
69 |
70 | @property
71 | def imperial_unit(self) -> str | None:
72 | """Return the imperial unit."""
73 | return (
74 | self.unit_i
75 | if self.unit_i_cnv is None
76 | else no_conversion_to_none(self.unit_i_cnv)
77 | )
78 |
79 | @property
80 | def metric_unit(self) -> str | None:
81 | """Return the metric unit."""
82 | return (
83 | self.unit_m
84 | if self.unit_m_cnv is None
85 | else no_conversion_to_none(self.unit_m_cnv)
86 | )
87 |
88 |
89 | @dataclass
90 | class SensorDescription(BaseSensorDescription):
91 | """Sensor description."""
92 |
93 | custom_fn: Callable[[ConversionFunctions, WeatherFlowDevice], Any] | Callable[
94 | [ConversionFunctions, WeatherFlowDevice, Any | None], Any
95 | ] | None = None
96 | decimals: tuple[int | None, int | None] = (None, None)
97 | inputs: tuple[str, ...] = field(default_factory=tuple[str, ...])
98 |
99 |
100 | @dataclass
101 | class SqlSensorDescription(BaseSensorDescription):
102 | """Sql-based sensor description."""
103 |
104 | sql_fn: Callable[[SQLFunctions], Any] | None = None
105 |
106 |
107 | @dataclass
108 | class StorageSensorDescription(BaseSensorDescription):
109 | """Storage-based sensor description."""
110 |
111 | storage_field: str | None = None
112 |
113 | cnv_fn: Callable[[ConversionFunctions, Any], Any] | None = None
114 |
115 | def value(self, storage: dict[str, Any]) -> Any:
116 | """Return the field value from the storage."""
117 | return storage[self.storage_field]
118 |
119 |
120 | STATUS_SENSOR = SensorDescription(
121 | id="status",
122 | name="Status",
123 | icon="clock-outline",
124 | event=EVENT_STATUS_UPDATE,
125 | attr="uptime",
126 | device_class=DEVICE_CLASS_TIMESTAMP,
127 | )
128 |
129 | DEVICE_SENSORS: tuple[BaseSensorDescription, ...] = (
130 | STATUS_SENSOR,
131 | SensorDescription(
132 | id="absolute_humidity",
133 | name="Absolute Humidity",
134 | event=EVENT_OBSERVATION,
135 | unit_m="g/m³",
136 | unit_i="g/m³",
137 | state_class=STATE_CLASS_MEASUREMENT,
138 | icon="water-opacity",
139 | attr="relative_humidity",
140 | custom_fn=lambda cnv, device: None
141 | if None in (device.air_temperature, device.relative_humidity)
142 | else cnv.absolute_humidity(
143 | device.air_temperature.m, device.relative_humidity.m
144 | ),
145 | ),
146 | SensorDescription(
147 | id="air_density",
148 | name="Air Density",
149 | event=EVENT_OBSERVATION,
150 | unit_m="kg/m³",
151 | unit_i="lb/ft³",
152 | state_class=STATE_CLASS_MEASUREMENT,
153 | icon="air-filter",
154 | decimals=(5, 5),
155 | ),
156 | SensorDescription(
157 | id="air_temperature",
158 | name="Temperature",
159 | unit_m=TEMP_CELSIUS,
160 | unit_i=TEMP_FAHRENHEIT,
161 | device_class=DEVICE_CLASS_TEMPERATURE,
162 | state_class=STATE_CLASS_MEASUREMENT,
163 | event=EVENT_OBSERVATION,
164 | extra_att=True,
165 | show_min_att=True,
166 | decimals=(1, 1),
167 | ),
168 | SensorDescription(
169 | id="battery",
170 | name="Voltage",
171 | unit_m="V",
172 | unit_i="V",
173 | device_class=DEVICE_CLASS_VOLTAGE,
174 | state_class=STATE_CLASS_MEASUREMENT,
175 | event=EVENT_OBSERVATION,
176 | decimals=(2, 2),
177 | ),
178 | SensorDescription(
179 | id="battery_level",
180 | name="Battery",
181 | unit_m="%",
182 | unit_i="%",
183 | device_class=DEVICE_CLASS_BATTERY,
184 | state_class=STATE_CLASS_MEASUREMENT,
185 | event=EVENT_OBSERVATION,
186 | attr="battery",
187 | custom_fn=lambda cnv, device: None
188 | if device.battery is None
189 | else cnv.battery_level(device.battery.m, isinstance(device, TempestDevice)),
190 | ),
191 | SensorDescription(
192 | id="battery_mode",
193 | name="Battery Mode",
194 | icon="information-outline",
195 | event=EVENT_OBSERVATION,
196 | attr="battery",
197 | has_description=True,
198 | custom_fn=lambda cnv, device: (None, None)
199 | if None in (device.battery, device.solar_radiation)
200 | else cnv.battery_mode(device.battery.m, device.solar_radiation.m),
201 | ),
202 | SensorDescription(
203 | id="beaufort",
204 | name="Beaufort Scale",
205 | state_class=STATE_CLASS_MEASUREMENT,
206 | icon="tailwind",
207 | event=EVENT_OBSERVATION,
208 | attr="wind_speed",
209 | custom_fn=lambda cnv, device: (None, None)
210 | if device.wind_speed is None
211 | else cnv.beaufort(device.wind_speed.m),
212 | has_description=True,
213 | ),
214 | SensorDescription(
215 | id="cloud_base",
216 | name="Cloud Base Altitude",
217 | icon="weather-cloudy",
218 | unit_m=ALTITUDE_METERS,
219 | unit_i=ALTITUDE_FEET,
220 | state_class=STATE_CLASS_MEASUREMENT,
221 | event=EVENT_OBSERVATION,
222 | attr="calculate_cloud_base",
223 | decimals=(0, 0),
224 | inputs=("altitude",),
225 | ),
226 | SensorDescription(
227 | id="delta_t",
228 | name="Delta T",
229 | unit_m=TEMP_CELSIUS,
230 | unit_m_cnv="delta_degC",
231 | unit_i=TEMP_FAHRENHEIT,
232 | unit_i_cnv="delta_degF",
233 | device_class=DEVICE_CLASS_TEMPERATURE,
234 | state_class=STATE_CLASS_MEASUREMENT,
235 | event=EVENT_OBSERVATION,
236 | decimals=(1, 1),
237 | ),
238 | SensorDescription(
239 | id="dewpoint",
240 | name="Dew Point",
241 | unit_m=TEMP_CELSIUS,
242 | unit_i=TEMP_FAHRENHEIT,
243 | device_class=DEVICE_CLASS_TEMPERATURE,
244 | state_class=STATE_CLASS_MEASUREMENT,
245 | icon="thermometer-lines",
246 | event=EVENT_OBSERVATION,
247 | extra_att=True,
248 | show_min_att=True,
249 | attr="dew_point_temperature",
250 | decimals=(1, 1),
251 | ),
252 | SensorDescription(
253 | id="dewpoint_description",
254 | name="Dewpoint Comfort Level",
255 | icon="text-box-outline",
256 | event=EVENT_OBSERVATION,
257 | attr="dew_point_temperature",
258 | custom_fn=lambda cnv, device: None
259 | if device.dew_point_temperature is None
260 | else cnv.dewpoint_level(device.dew_point_temperature.m, True),
261 | ),
262 | SensorDescription(
263 | id="feelslike",
264 | name="Feels Like Temperature",
265 | unit_m=TEMP_CELSIUS,
266 | unit_i=TEMP_FAHRENHEIT,
267 | device_class=DEVICE_CLASS_TEMPERATURE,
268 | state_class=STATE_CLASS_MEASUREMENT,
269 | event=EVENT_OBSERVATION,
270 | attr="air_temperature",
271 | decimals=(1, 1),
272 | custom_fn=lambda cnv, device, wind_speed: None
273 | if wind_speed is None
274 | or None in (device.air_temperature, device.relative_humidity)
275 | else cnv.feels_like(
276 | device.air_temperature.m, device.relative_humidity.m, wind_speed
277 | ),
278 | ),
279 | SensorDescription(
280 | id="freezing_level",
281 | name="Freezing Level Altitude",
282 | icon="altimeter",
283 | unit_m=ALTITUDE_METERS,
284 | unit_i=ALTITUDE_FEET,
285 | state_class=STATE_CLASS_MEASUREMENT,
286 | event=EVENT_OBSERVATION,
287 | attr="calculate_freezing_level",
288 | decimals=(0, 0),
289 | inputs=("altitude",),
290 | ),
291 | SensorDescription(
292 | id="illuminance",
293 | name="Illuminance",
294 | unit_m="lx",
295 | unit_i="lx",
296 | device_class=DEVICE_CLASS_ILLUMINANCE,
297 | state_class=STATE_CLASS_MEASUREMENT,
298 | event=EVENT_OBSERVATION,
299 | extra_att=True,
300 | ),
301 | SensorDescription(
302 | id="lightning_strike_count",
303 | name="Lightning Count",
304 | icon="weather-lightning",
305 | event=EVENT_OBSERVATION,
306 | ),
307 | SqlSensorDescription(
308 | id="lightning_strike_count_1hr",
309 | name="Lightning Count (Last hour)",
310 | icon="weather-lightning",
311 | event=EVENT_OBSERVATION,
312 | attr="lightning_strike_count",
313 | sql_fn=lambda sql: sql.readLightningCount(1),
314 | ),
315 | SqlSensorDescription(
316 | id="lightning_strike_count_3hr",
317 | name="Lightning Count (3 hours)",
318 | icon="weather-lightning",
319 | event=EVENT_OBSERVATION,
320 | attr="lightning_strike_count",
321 | sql_fn=lambda sql: sql.readLightningCount(3),
322 | ),
323 | StorageSensorDescription(
324 | id="lightning_strike_count_today",
325 | name="Lightning Count (Today)",
326 | state_class=STATE_CLASS_MEASUREMENT,
327 | icon="weather-lightning",
328 | event=EVENT_OBSERVATION,
329 | extra_att=True,
330 | last_reset=True,
331 | attr="lightning_strike_count",
332 | storage_field="lightning_count_today",
333 | ),
334 | StorageSensorDescription(
335 | id="lightning_strike_distance",
336 | name="Lightning Distance",
337 | unit_m="km",
338 | unit_i="mi",
339 | state_class=STATE_CLASS_MEASUREMENT,
340 | icon="flash",
341 | event=EVENT_OBSERVATION,
342 | attr="lightning_strike_average_distance",
343 | storage_field="last_lightning_distance",
344 | ),
345 | StorageSensorDescription(
346 | id="lightning_strike_energy",
347 | name="Lightning Energy",
348 | state_class=STATE_CLASS_MEASUREMENT,
349 | icon="flash",
350 | event=EVENT_OBSERVATION,
351 | extra_att=True,
352 | attr="last_lightning_strike_event",
353 | storage_field="last_lightning_energy",
354 | ),
355 | StorageSensorDescription(
356 | id="lightning_strike_time",
357 | name="Last Lightning Strike",
358 | device_class=DEVICE_CLASS_TIMESTAMP,
359 | icon="clock-outline",
360 | event="lightning_strike_time",
361 | attr="last_lightning_strike_event",
362 | storage_field="last_lightning_time",
363 | cnv_fn=lambda cnv, val: cnv.utc_from_timestamp(val),
364 | ),
365 | SensorDescription(
366 | id="precipitation_type",
367 | name="Precipitation Type",
368 | icon="weather-rainy",
369 | event=EVENT_OBSERVATION,
370 | custom_fn=lambda cnv, device: None
371 | if device.precipitation_type is None
372 | else cnv.rain_type(device.precipitation_type.value),
373 | ),
374 | SensorDescription(
375 | id="pressure_trend",
376 | name="Pressure Trend",
377 | icon="trending-up",
378 | event=EVENT_OBSERVATION,
379 | attr="station_pressure",
380 | ),
381 | StorageSensorDescription(
382 | id="rain_duration_today",
383 | name="Rain Duration (Today)",
384 | unit_m="min",
385 | unit_i="min",
386 | state_class=STATE_CLASS_MEASUREMENT,
387 | icon="timeline-clock-outline",
388 | event=EVENT_OBSERVATION,
389 | extra_att=True,
390 | last_reset=True,
391 | attr="rain_accumulation_previous_minute",
392 | storage_field="rain_duration_today",
393 | ),
394 | StorageSensorDescription(
395 | id="rain_duration_yesterday",
396 | name="Rain Duration (Yesterday)",
397 | unit_m="min",
398 | unit_i="min",
399 | icon="timeline-clock-outline",
400 | event=EVENT_OBSERVATION,
401 | attr="rain_accumulation_previous_minute",
402 | storage_field="rain_duration_yesterday",
403 | ),
404 | SensorDescription(
405 | id="rain_intensity",
406 | name="Rain Intensity",
407 | icon="text-box-outline",
408 | event=EVENT_OBSERVATION,
409 | attr="rain_accumulation_previous_minute",
410 | custom_fn=lambda cnv, device: None
411 | if device.rain_rate is None
412 | else cnv.rain_intensity(device.rain_rate.m),
413 | ),
414 | SensorDescription(
415 | id="rain_rate",
416 | name="Rain Rate",
417 | unit_m="mm/h",
418 | unit_m_cnv="mm/h",
419 | unit_i="in/h",
420 | unit_i_cnv="in/h",
421 | device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY,
422 | state_class=STATE_CLASS_MEASUREMENT,
423 | icon="weather-pouring",
424 | event=EVENT_OBSERVATION,
425 | extra_att=True,
426 | decimals=(2, 2),
427 | ),
428 | StorageSensorDescription(
429 | id="rain_start_time",
430 | name="Last Rain start",
431 | device_class=DEVICE_CLASS_TIMESTAMP,
432 | icon="clock-outline",
433 | event="rain_start_time",
434 | attr="last_rain_start_event",
435 | storage_field="rain_start",
436 | cnv_fn=lambda cnv, val: cnv.utc_from_timestamp(val),
437 | ),
438 | StorageSensorDescription(
439 | id="rain_today",
440 | name="Rain Today",
441 | unit_m="mm",
442 | unit_i="in",
443 | device_class=DEVICE_CLASS_PRECIPITATION,
444 | state_class=STATE_CLASS_MEASUREMENT,
445 | icon="weather-pouring",
446 | event=EVENT_OBSERVATION,
447 | last_reset=True,
448 | attr="rain_accumulation_previous_minute",
449 | storage_field="rain_today",
450 | cnv_fn=lambda cnv, val: cnv.rain(val),
451 | ),
452 | StorageSensorDescription(
453 | id="rain_yesterday",
454 | name="Rain Yesterday",
455 | unit_m="mm",
456 | unit_i="in",
457 | icon="weather-pouring",
458 | device_class=DEVICE_CLASS_PRECIPITATION,
459 | event=EVENT_OBSERVATION,
460 | attr="rain_accumulation_previous_minute",
461 | storage_field="rain_yesterday",
462 | cnv_fn=lambda cnv, val: cnv.rain(val),
463 | ),
464 | SensorDescription(
465 | id="relative_humidity",
466 | name="Humidity",
467 | unit_m="%",
468 | unit_m_cnv=NO_CONVERSION,
469 | unit_i="%",
470 | unit_i_cnv=NO_CONVERSION,
471 | device_class=DEVICE_CLASS_HUMIDITY,
472 | state_class=STATE_CLASS_MEASUREMENT,
473 | event=EVENT_OBSERVATION,
474 | extra_att=True,
475 | show_min_att=True,
476 | ),
477 | SensorDescription(
478 | id="sealevel_pressure",
479 | name="Sea Level Pressure",
480 | unit_m="hPa",
481 | unit_i="inHg",
482 | device_class=DEVICE_CLASS_PRESSURE,
483 | state_class=STATE_CLASS_MEASUREMENT,
484 | event=EVENT_OBSERVATION,
485 | extra_att=True,
486 | show_min_att=True,
487 | attr="calculate_sea_level_pressure",
488 | decimals=(2, 3),
489 | inputs=("altitude",),
490 | ),
491 | SensorDescription(
492 | id="solar_radiation",
493 | name="Solar Radiation",
494 | unit_m="W/m²",
495 | unit_i="W/m²",
496 | device_class=DEVICE_CLASS_IRRADIANCE,
497 | state_class=STATE_CLASS_MEASUREMENT,
498 | icon="solar-power",
499 | event=EVENT_OBSERVATION,
500 | extra_att=True,
501 | ),
502 | SensorDescription(
503 | id="station_pressure",
504 | name="Station Pressure",
505 | unit_m="hPa",
506 | unit_i="inHg",
507 | device_class=DEVICE_CLASS_PRESSURE,
508 | state_class=STATE_CLASS_MEASUREMENT,
509 | event=EVENT_OBSERVATION,
510 | decimals=(2, 3),
511 | ),
512 | SensorDescription(
513 | id="temperature_description",
514 | name="Temperature Level",
515 | icon="text-box-outline",
516 | event=EVENT_OBSERVATION,
517 | attr="air_temperature",
518 | custom_fn=lambda cnv, device: None
519 | if device.air_temperature is None
520 | else cnv.temperature_level(device.air_temperature.m),
521 | ),
522 | SensorDescription(
523 | id="uv",
524 | name="UV Index",
525 | unit_m=UV_INDEX,
526 | unit_i=UV_INDEX,
527 | state_class=STATE_CLASS_MEASUREMENT,
528 | icon="weather-sunny-alert",
529 | event=EVENT_OBSERVATION,
530 | extra_att=True,
531 | ),
532 | SensorDescription(
533 | id="uv_description",
534 | name="UV Level",
535 | icon="text-box-outline",
536 | event=EVENT_OBSERVATION,
537 | attr="uv",
538 | custom_fn=lambda cnv, device: cnv.uv_level(device.uv),
539 | ),
540 | SensorDescription(
541 | id="visibility",
542 | name="Visibility",
543 | unit_m="km",
544 | unit_i="mi",
545 | device_class=DEVICE_CLASS_DISTANCE,
546 | state_class=STATE_CLASS_MEASUREMENT,
547 | icon="eye",
548 | event=EVENT_OBSERVATION,
549 | attr="air_temperature",
550 | custom_fn=lambda cnv, device, elevation: None
551 | if None in (device.air_temperature, device.relative_humidity)
552 | else cnv.visibility(
553 | elevation, device.air_temperature.m, device.relative_humidity.m
554 | ),
555 | ),
556 | SensorDescription(
557 | id="wbgt",
558 | name="Wet Bulb Globe Temperature",
559 | unit_m=TEMP_CELSIUS,
560 | unit_i=TEMP_FAHRENHEIT,
561 | device_class=DEVICE_CLASS_TEMPERATURE,
562 | state_class=STATE_CLASS_MEASUREMENT,
563 | event=EVENT_OBSERVATION,
564 | attr="wet_bulb_temperature",
565 | decimals=(1, 1),
566 | custom_fn=lambda cnv, device, solar_radiation: None
567 | if None
568 | in (device.air_temperature, device.relative_humidity, device.station_pressure)
569 | else cnv.wbgt(
570 | device.air_temperature.m,
571 | device.relative_humidity.m,
572 | device.station_pressure.m,
573 | solar_radiation,
574 | ),
575 | ),
576 | SensorDescription(
577 | id="wetbulb",
578 | name="Wet Bulb Temperature",
579 | unit_m=TEMP_CELSIUS,
580 | unit_i=TEMP_FAHRENHEIT,
581 | device_class=DEVICE_CLASS_TEMPERATURE,
582 | state_class=STATE_CLASS_MEASUREMENT,
583 | event=EVENT_OBSERVATION,
584 | attr="wet_bulb_temperature",
585 | decimals=(1, 1),
586 | ),
587 | SensorDescription(
588 | id="wind_bearing",
589 | name="Wind Bearing",
590 | unit_m="°",
591 | unit_i="°",
592 | state_class=STATE_CLASS_MEASUREMENT,
593 | icon="compass",
594 | event=EVENT_RAPID_WIND,
595 | attr="wind_direction",
596 | ),
597 | SensorDescription(
598 | id="wind_bearing_avg",
599 | name="Wind Bearing Avg",
600 | unit_m="°",
601 | unit_i="°",
602 | state_class=STATE_CLASS_MEASUREMENT,
603 | icon="compass",
604 | event=EVENT_OBSERVATION,
605 | attr="wind_direction",
606 | ),
607 | SensorDescription(
608 | id="wind_direction",
609 | name="Wind Direction",
610 | icon="compass-outline",
611 | event=EVENT_RAPID_WIND,
612 | ),
613 | SensorDescription(
614 | id="wind_direction_avg",
615 | name="Wind Direction Avg",
616 | icon="compass-outline",
617 | event=EVENT_OBSERVATION,
618 | attr="wind_direction",
619 | custom_fn=lambda cnv, device: None
620 | if device.wind_direction is None
621 | else cnv.direction(device.wind_direction.m),
622 | ),
623 | SensorDescription(
624 | id="wind_gust",
625 | name="Wind Gust",
626 | unit_m="m/s",
627 | unit_i="mph",
628 | device_class=DEVICE_CLASS_WIND_SPEED,
629 | state_class=STATE_CLASS_MEASUREMENT,
630 | icon="weather-windy",
631 | event=EVENT_OBSERVATION,
632 | extra_att=True,
633 | decimals=(1, 2),
634 | ),
635 | SensorDescription(
636 | id="wind_lull",
637 | name="Wind Lull",
638 | unit_m="m/s",
639 | unit_i="mph",
640 | device_class=DEVICE_CLASS_WIND_SPEED,
641 | state_class=STATE_CLASS_MEASUREMENT,
642 | icon="weather-windy-variant",
643 | event=EVENT_OBSERVATION,
644 | extra_att=True,
645 | decimals=(1, 2),
646 | ),
647 | SensorDescription(
648 | id="wind_speed",
649 | name="Wind Speed",
650 | unit_m="m/s",
651 | unit_i="mph",
652 | device_class=DEVICE_CLASS_WIND_SPEED,
653 | state_class=STATE_CLASS_MEASUREMENT,
654 | icon="weather-windy",
655 | event=EVENT_RAPID_WIND,
656 | decimals=(1, 2),
657 | ),
658 | SensorDescription(
659 | id="wind_speed_avg",
660 | name="Wind Speed Avg",
661 | unit_m="m/s",
662 | unit_i="mph",
663 | device_class=DEVICE_CLASS_WIND_SPEED,
664 | state_class=STATE_CLASS_MEASUREMENT,
665 | icon="weather-windy-variant",
666 | event=EVENT_OBSERVATION,
667 | extra_att=True,
668 | attr="wind_average",
669 | decimals=(1, 2),
670 | ),
671 | SensorDescription(
672 | id="solar_elevation",
673 | name="Solar Elevation",
674 | unit_m="°",
675 | unit_i="°",
676 | state_class=STATE_CLASS_MEASUREMENT,
677 | icon="angle-acute",
678 | event=EVENT_OBSERVATION,
679 | attr="solar_radiation",
680 | custom_fn=lambda cnv, latitude, longitude: None
681 | if None in (latitude, longitude)
682 | else cnv.solar_elevation(latitude, longitude)
683 | ),
684 | SensorDescription(
685 | id="solar_insolation",
686 | name="Solar Insolation",
687 | unit_m="W/m²",
688 | unit_i="W/m²",
689 | state_class=STATE_CLASS_MEASUREMENT,
690 | icon="solar-power",
691 | event=EVENT_OBSERVATION,
692 | attr="solar_radiation",
693 | custom_fn=lambda cnv, elevation, latitude, longitude: None
694 | if None in (elevation, latitude, longitude)
695 | else cnv.solar_insolation(elevation, latitude, longitude)
696 | ),
697 | SensorDescription(
698 | id="zambretti_number",
699 | name="Zambretti Number",
700 | state_class=STATE_CLASS_MEASUREMENT,
701 | icon="vector-bezier",
702 | event=EVENT_OBSERVATION,
703 | attr="station_pressure",
704 | custom_fn=lambda cnv, latitude, wind_direction_avg, p_hi, p_lo, pressure_trend, sealevel_pressure: None
705 | if None in (latitude, wind_direction_avg, p_hi, p_lo, pressure_trend, sealevel_pressure)
706 | else cnv.zambretti_value(latitude, wind_direction_avg, p_hi, p_lo, pressure_trend, sealevel_pressure)
707 | ),
708 | SensorDescription(
709 | id="zambretti_text",
710 | name="Zambretti Text",
711 | icon="vector-bezier",
712 | event=EVENT_OBSERVATION,
713 | attr="station_pressure",
714 | custom_fn=lambda cnv, zambretti_value: None
715 | if zambretti_value is None
716 | else cnv.zambretti_forecast(zambretti_value),
717 | ),
718 | SensorDescription(
719 | id="fog_probability",
720 | name="Fog Probability",
721 | unit_m="%",
722 | unit_i="%",
723 | state_class=STATE_CLASS_MEASUREMENT,
724 | icon="weather-fog",
725 | event=EVENT_OBSERVATION,
726 | attr="relative_humidity",
727 | custom_fn=lambda cnv, solar_elevation, wind_speed, humidity, dew_point, air_temperature: 0
728 | if None in (solar_elevation, wind_speed, humidity, dew_point, air_temperature)
729 | else cnv.fog_probability(solar_elevation, wind_speed, humidity, dew_point, air_temperature),
730 | ),
731 | SensorDescription(
732 | id="snow_probability",
733 | name="Snow Probability",
734 | unit_m="%",
735 | unit_i="%",
736 | state_class=STATE_CLASS_MEASUREMENT,
737 | icon="snowflake",
738 | event=EVENT_OBSERVATION,
739 | attr="relative_humidity",
740 | custom_fn=lambda cnv, device, freezing_level, cloud_base, elevation: None
741 | if None in (device.air_temperature, device.dew_point_temperature, device.wet_bulb_temperature, freezing_level, cloud_base)
742 | else cnv.snow_probability(device.air_temperature.m, freezing_level, cloud_base, device.dew_point_temperature.m, device.wet_bulb_temperature.m, elevation)
743 | ),
744 |
745 | SensorDescription(
746 | id="current_conditions",
747 | name="Current Conditions",
748 | icon="weather-partly-snowy-rainy",
749 | event=EVENT_OBSERVATION,
750 | attr="rain_rate",
751 | custom_fn=lambda cnv, lightning_strike_count_1hr, precipitation_type, rain_rate, wind_speed, solar_elevation, solar_radiation, solar_insolation, snow_probability, fog_probability: "clear-night"
752 | if None in (lightning_strike_count_1hr, precipitation_type, rain_rate, wind_speed, solar_elevation, solar_radiation, solar_insolation, snow_probability, fog_probability)
753 | else cnv.current_conditions(lightning_strike_count_1hr, precipitation_type, rain_rate, wind_speed, solar_elevation, solar_radiation, solar_insolation, snow_probability, fog_probability)
754 | ),
755 | )
756 |
757 | HUB_SENSORS: tuple[BaseSensorDescription, ...] = (STATUS_SENSOR,)
758 |
759 | FORECAST_SENSORS: tuple[BaseSensorDescription, ...] = (
760 | SensorDescription(
761 | id=FORECAST_ENTITY,
762 | name="Weather",
763 | icon="chart-box-outline",
764 | event=EVENT_FORECAST,
765 | ),
766 | )
767 |
768 | OBSOLETE_SENSORS = ["uptime"]
769 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/sqlite.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import json
3 | import logging
4 | import os.path
5 | import sqlite3
6 | import time
7 | from datetime import timezone
8 | from sqlite3 import Error as SQLError
9 | from typing import OrderedDict
10 |
11 | from .const import (
12 | COL_DEWPOINT,
13 | COL_HUMIDITY,
14 | COL_ILLUMINANCE,
15 | COL_PRESSURE,
16 | COL_RAINDURATION,
17 | COL_RAINRATE,
18 | COL_SOLARRAD,
19 | COL_STRIKECOUNT,
20 | COL_STRIKEENERGY,
21 | COL_TEMPERATURE,
22 | COL_UV,
23 | COL_WINDGUST,
24 | COL_WINDLULL,
25 | COL_WINDSPEED,
26 | DATABASE_VERSION,
27 | PRESSURE_TREND_TIMER,
28 | STORAGE_FILE,
29 | STORAGE_ID,
30 | STRIKE_COUNT_TIMER,
31 | TABLE_HIGH_LOW,
32 | TABLE_LIGHTNING,
33 | TABLE_PRESSURE,
34 | TABLE_STORAGE,
35 | UNITS_IMPERIAL,
36 | UTC,
37 | )
38 |
39 | _LOGGER = logging.getLogger(__name__)
40 |
41 |
42 | class SQLFunctions:
43 | """Class to handle SQLLite functions."""
44 |
45 | def __init__(self, unit_system, debug=False):
46 | """Initialize SQLFunctions."""
47 | self.connection = None
48 | self._unit_system = unit_system
49 | self._debug = debug
50 |
51 | def create_connection(self, db_file):
52 | """Create a database connection to a SQLite database."""
53 | try:
54 | self.connection = sqlite3.connect(db_file)
55 |
56 | except SQLError as e:
57 | _LOGGER.error("Could not create SQL Database. Error: %s", e)
58 |
59 | def create_table(self, create_table_sql):
60 | """Create table from the create_table_sql statement.
61 |
62 | :param conn: Connection object
63 | :param create_table_sql: a CREATE TABLE statement
64 | :return:
65 | """
66 | try:
67 | c = self.connection.cursor()
68 | c.execute(create_table_sql)
69 | except SQLError as e:
70 | _LOGGER.error("Could not create SQL Table. Error: %s", e)
71 |
72 | def create_storage_row(self, rowdata):
73 | """Create new storage row into the storage table.
74 |
75 | :param conn:
76 | :param rowdata:
77 | :return: project id
78 | """
79 | sql = """ INSERT INTO storage(id, rain_today, rain_yesterday, rain_start, rain_duration_today,
80 | rain_duration_yesterday, lightning_count, lightning_count_today, last_lightning_time,
81 | last_lightning_distance, last_lightning_energy)
82 | VALUES(?, ?,?,?,?,?,?,?,?,?,?) """
83 | try:
84 | cur = self.connection.cursor()
85 | cur.execute(sql, rowdata)
86 | self.connection.commit()
87 | return cur.lastrowid
88 | except SQLError as e:
89 | _LOGGER.error("Could not Insert data in table storage. Error: %s", e)
90 |
91 | def readStorage(self):
92 | """Return data from the storage table as JSON."""
93 | try:
94 | cursor = self.connection.cursor()
95 | cursor.execute(f"SELECT * FROM storage WHERE id = {STORAGE_ID};")
96 | data = cursor.fetchall()
97 |
98 | for row in data:
99 | storage_json = {
100 | "rain_today": row[1],
101 | "rain_yesterday": row[2],
102 | "rain_start": row[3],
103 | "rain_duration_today": row[4],
104 | "rain_duration_yesterday": row[5],
105 | "lightning_count": row[6],
106 | "lightning_count_today": row[7],
107 | "last_lightning_time": row[8],
108 | "last_lightning_distance": row[9],
109 | "last_lightning_energy": row[10],
110 | }
111 |
112 | return storage_json
113 |
114 | except SQLError as e:
115 | _LOGGER.error("Could not access storage data. Error: %s", e)
116 |
117 | def writeStorage(self, json_data: OrderedDict):
118 | """Store data in the storage table from JSON."""
119 | try:
120 | cursor = self.connection.cursor()
121 | sql_statement = """UPDATE storage
122 | SET rain_today=?,
123 | rain_yesterday=?,
124 | rain_start=?,
125 | rain_duration_today=?,
126 | rain_duration_yesterday=?,
127 | lightning_count=?,
128 | lightning_count_today=?,
129 | last_lightning_time=?,
130 | last_lightning_distance=?,
131 | last_lightning_energy=?
132 | WHERE ID = ?
133 | """
134 |
135 | rowdata = (
136 | json_data["rain_today"],
137 | json_data["rain_yesterday"],
138 | json_data["rain_start"],
139 | json_data["rain_duration_today"],
140 | json_data["rain_duration_yesterday"],
141 | json_data["lightning_count"],
142 | json_data["lightning_count_today"],
143 | json_data["last_lightning_time"],
144 | json_data["last_lightning_distance"],
145 | json_data["last_lightning_energy"],
146 | STORAGE_ID,
147 | )
148 |
149 | cursor.execute(sql_statement, rowdata)
150 | self.connection.commit()
151 |
152 | except SQLError as e:
153 | _LOGGER.error("Could not update storage data. Error: %s", e)
154 |
155 | def readPressureTrend(self, new_pressure, translations):
156 | """Return Pressure Trend."""
157 | if new_pressure is None:
158 | return "Steady", 0
159 |
160 | try:
161 | time_point = time.time() - PRESSURE_TREND_TIMER
162 | cursor = self.connection.cursor()
163 | cursor.execute(
164 | f"SELECT pressure FROM pressure WHERE timestamp < {time_point} ORDER BY timestamp DESC LIMIT 1;"
165 | )
166 | old_pressure = cursor.fetchone()
167 | if old_pressure is None:
168 | old_pressure = new_pressure
169 | else:
170 | old_pressure = float(old_pressure[0])
171 | pressure_delta = new_pressure - old_pressure
172 |
173 | min_value = -1
174 | max_value = 1
175 | if self._unit_system == UNITS_IMPERIAL:
176 | min_value = -0.0295
177 | max_value = 0.0295
178 |
179 | if pressure_delta > min_value and pressure_delta < max_value:
180 | return translations["trend"]["steady"], 0
181 | if pressure_delta <= min_value:
182 | return translations["trend"]["falling"], round(pressure_delta, 2)
183 | if pressure_delta >= max_value:
184 | return translations["trend"]["rising"], round(pressure_delta, 2)
185 |
186 | except SQLError as e:
187 | _LOGGER.error("Could not read pressure data. Error: %s", e)
188 | except Exception as e:
189 | _LOGGER.error("Could not calculate pressure trend. Error message: %s", e)
190 |
191 | def readPressureData(self):
192 | """Return formatted pressure data - USED FOR TESTING ONLY."""
193 | try:
194 | cursor = self.connection.cursor()
195 | cursor.execute("SELECT * FROM pressure;")
196 | data = cursor.fetchall()
197 |
198 | for row in data:
199 | tid = datetime.datetime.fromtimestamp(row[0]).isoformat()
200 | print(tid, row[1])
201 |
202 | except SQLError as e:
203 | _LOGGER.error("Could not access storage data. Error: %s", e)
204 |
205 | def writePressure(self, pressure):
206 | """Add entry to the Pressure Table."""
207 | try:
208 | cur = self.connection.cursor()
209 | cur.execute(
210 | f"INSERT INTO pressure(timestamp, pressure) VALUES({time.time()}, {pressure});"
211 | )
212 | self.connection.commit()
213 | return True
214 | except SQLError as e:
215 | _LOGGER.error("Could not Insert data in table Pressure. Error: %s", e)
216 | return False
217 | except Exception as e:
218 | _LOGGER.error("Could write to Pressure Table. Error message: %s", e)
219 | return False
220 |
221 | def readLightningCount(self, hours: int):
222 | """Return number of Lightning Strikes in the last x hours."""
223 | try:
224 | time_point = time.time() - hours * 60 * 60
225 | cursor = self.connection.cursor()
226 | cursor.execute(
227 | f"SELECT COUNT(*) FROM lightning WHERE timestamp > {time_point};"
228 | )
229 | data = cursor.fetchone()[0]
230 |
231 | return data
232 |
233 | except SQLError as e:
234 | _LOGGER.error("Could not access storage data. Error: %s", e)
235 |
236 | def writeLightning(self):
237 | """Adds an entry to the Lightning Table."""
238 |
239 | try:
240 | cur = self.connection.cursor()
241 | cur.execute(f"INSERT INTO lightning(timestamp) VALUES({time.time()});")
242 | self.connection.commit()
243 | return True
244 | except SQLError as e:
245 | _LOGGER.error("Could not Insert data in table Lightning. Error: %s", e)
246 | return False
247 | except Exception as e:
248 | _LOGGER.error("Could write to Lightning Table. Error message: %s", e)
249 | return False
250 |
251 | def writeDailyLog(self, sensor_data):
252 | """Add entry to the Daily Log Table."""
253 | try:
254 | data = json.loads(json.dumps(sensor_data))
255 | temp = data.get("air_temperature")
256 | pres = data.get("sealevel_pressure")
257 | wspeed = data.get("wind_speed_avg")
258 |
259 | cursor = self.connection.cursor()
260 | cursor.execute(
261 | f"INSERT INTO daily_log(timestamp, temperature, pressure, windspeed) VALUES({time.time()}, ?, ?, ?)",
262 | (temp, pres, wspeed),
263 | )
264 | self.connection.commit()
265 |
266 | except SQLError as e:
267 | _LOGGER.error("Could not Insert data in table daily_log. Error: %s", e)
268 | except Exception as e:
269 | _LOGGER.error("Could not write to daily_log Table. Error message: %s", e)
270 |
271 | def updateDayData(self, sensor_data):
272 | """Update Day Data Table."""
273 | try:
274 | data = json.loads(json.dumps(sensor_data))
275 | temp = data.get("air_temperature")
276 | pres = data.get("sealevel_pressure")
277 | wspeed = data.get("wind_speed_avg")
278 | hum = data.get("relative_humidity")
279 | dew = data.get("dewpoint")
280 | illum = data.get("illuminance")
281 | rain_dur = data.get("rain_duration_today")
282 | rain_rate = data.get("rain_rate")
283 | wgust = data.get("wind_gust")
284 | wlull = data.get("wind_lull")
285 | strike_e = data.get("lightning_strike_energy")
286 | strike_c = data.get("lightning_strike_count_today")
287 | uv = data.get("uv")
288 | solrad = data.get("solar_radiation")
289 |
290 | cursor = self.connection.cursor()
291 | sql_columns = "INSERT INTO day_data("
292 | sql_columns += "timestamp, air_temperature, sealevel_pressure, wind_speed_avg, relative_humidity, dewpoint,"
293 | sql_columns += "illuminance, rain_duration_today, rain_rate, wind_gust, wind_lull, lightning_strike_energy,"
294 | sql_columns += "lightning_strike_count_today, uv, solar_radiation"
295 | sql_columns += ")"
296 | cursor.execute(
297 | f"{sql_columns} VALUES({time.time()}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
298 | (
299 | temp,
300 | pres,
301 | wspeed,
302 | hum,
303 | dew,
304 | illum,
305 | rain_dur,
306 | rain_rate,
307 | wgust,
308 | wlull,
309 | strike_e,
310 | strike_c,
311 | uv,
312 | solrad,
313 | ),
314 | )
315 | self.connection.commit()
316 |
317 | except SQLError as e:
318 | _LOGGER.error("Could not Insert data in table day_data. Error: %s", e)
319 | except Exception as e:
320 | _LOGGER.error("Could not write to day_data Table. Error message: %s", e)
321 |
322 | def updateHighLow(self, sensor_data):
323 | """Update High and Low Values."""
324 | try:
325 | self.connection.row_factory = sqlite3.Row
326 | cursor = self.connection.cursor()
327 | cursor.execute("SELECT * FROM high_low;")
328 | table_data = cursor.fetchall()
329 |
330 | data = dict(sensor_data)
331 |
332 | for row in table_data:
333 | max_sql = None
334 | min_sql = None
335 | sensor_value = None
336 | do_update = False
337 | # Get Value of Sensor if available
338 | if data.get(row["sensorid"]) is not None:
339 | sensor_value = data[row["sensorid"]]
340 |
341 | # If we have a value, check if min/max changes
342 | if sensor_value is not None:
343 | if sensor_value > row["max_day"]:
344 | max_sql = (
345 | f" max_day = {sensor_value}, max_day_time = {time.time()} "
346 | )
347 | do_update = True
348 | if sensor_value < row["min_day"]:
349 | min_sql = (
350 | f" min_day = {sensor_value}, min_day_time = {time.time()} "
351 | )
352 | do_update = True
353 |
354 | # If min/max changes, update the record
355 | sql = "UPDATE high_low SET"
356 | if do_update:
357 | if max_sql:
358 | sql = f"{sql} {max_sql}"
359 | if max_sql and min_sql:
360 | sql = f"{sql},"
361 | if min_sql:
362 | sql = f"{sql} {min_sql}"
363 | sql = f"{sql}, latest = {sensor_value} WHERE sensorid = '{row['sensorid']}'"
364 | if self._debug:
365 | _LOGGER.debug("Min/Max SQL: %s", sql)
366 | cursor.execute(sql)
367 | self.connection.commit()
368 | else:
369 | if sensor_value is not None:
370 | sql = f"{sql} latest = {sensor_value} WHERE sensorid = '{row['sensorid']}'"
371 | if self._debug:
372 | _LOGGER.debug("Latest SQL: %s", sql)
373 | cursor.execute(sql)
374 | self.connection.commit()
375 |
376 | except SQLError as e:
377 | _LOGGER.error("Could not update High and Low data. Error: %s", e)
378 | return False
379 | except Exception as e:
380 | _LOGGER.error("Could not write to High and Low Table. Error message: %s", e)
381 | return False
382 |
383 | def readHighLow(self):
384 | """Return data from the high_low table as JSON."""
385 | try:
386 | self.connection.row_factory = sqlite3.Row
387 | cursor = self.connection.cursor()
388 | cursor.execute("SELECT * FROM high_low")
389 | data = cursor.fetchall()
390 |
391 | sensor_json = {}
392 | for row in data:
393 | sensor_json[row["sensorid"]] = {
394 | "max_day": row["max_day"],
395 | "max_day_time": None
396 | if not row["max_day_time"]
397 | else datetime.datetime.utcfromtimestamp(round(row["max_day_time"]))
398 | .replace(tzinfo=UTC)
399 | .isoformat(),
400 | "max_month": row["max_month"],
401 | "max_month_time": None
402 | if not row["max_month_time"]
403 | else datetime.datetime.utcfromtimestamp(
404 | round(row["max_month_time"])
405 | )
406 | .replace(tzinfo=UTC)
407 | .isoformat(),
408 | "max_all": row["max_all"],
409 | "max_all_time": None
410 | if not row["max_all_time"]
411 | else datetime.datetime.utcfromtimestamp(round(row["max_all_time"]))
412 | .replace(tzinfo=UTC)
413 | .isoformat(),
414 | "min_day": row["min_day"],
415 | "min_day_time": None
416 | if not row["min_day_time"]
417 | else datetime.datetime.utcfromtimestamp(round(row["min_day_time"]))
418 | .replace(tzinfo=UTC)
419 | .isoformat(),
420 | "min_month": row["min_month"],
421 | "min_month_time": None
422 | if not row["min_month_time"]
423 | else datetime.datetime.utcfromtimestamp(
424 | round(row["min_month_time"])
425 | )
426 | .replace(tzinfo=UTC)
427 | .isoformat(),
428 | "min_all": row["min_all"],
429 | "min_all_time": None
430 | if not row["min_all_time"]
431 | else datetime.datetime.utcfromtimestamp(round(row["min_all_time"]))
432 | .replace(tzinfo=UTC)
433 | .isoformat(),
434 | }
435 | return sensor_json
436 |
437 | except SQLError as e:
438 | _LOGGER.error("Could not access high_low data. Error: %s", e)
439 | return None
440 | except Exception as e:
441 | _LOGGER.error("Could not get all High Low values. Error message: %s", e)
442 | return sensor_json
443 |
444 | def migrateStorageFile(self):
445 | """Migrate old .storage.json file to the database."""
446 | try:
447 | with open(STORAGE_FILE, "r") as jsonFile:
448 | old_data = json.load(jsonFile)
449 |
450 | # We need to convert the Rain Start to timestamp
451 | dt = datetime.datetime.strptime(
452 | old_data["rain_start"], "%Y-%m-%dT%H:%M:%S"
453 | )
454 | timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
455 |
456 | storage_json = {
457 | "rain_today": old_data["rain_today"],
458 | "rain_yesterday": old_data["rain_yesterday"],
459 | "rain_start": timestamp,
460 | "rain_duration_today": old_data["rain_duration_today"],
461 | "rain_duration_yesterday": old_data["rain_duration_yesterday"],
462 | "lightning_count": old_data["lightning_count"],
463 | "lightning_count_today": old_data["lightning_count_today"],
464 | "last_lightning_time": old_data["last_lightning_time"],
465 | "last_lightning_distance": old_data["last_lightning_distance"],
466 | "last_lightning_energy": old_data["last_lightning_energy"],
467 | }
468 |
469 | self.writeStorage(storage_json)
470 |
471 | except FileNotFoundError as e:
472 | _LOGGER.error("Could not find old storage file. Error message: %s", e)
473 | except Exception as e:
474 | _LOGGER.error("Could not Read storage file. Error message: %s", e)
475 |
476 | def createInitialDataset(self):
477 | """Initialize Initial database, and migrate data if needed."""
478 | try:
479 | with self.connection:
480 | # Create Empty Tables
481 | self.create_table(TABLE_STORAGE)
482 | self.create_table(TABLE_LIGHTNING)
483 | self.create_table(TABLE_PRESSURE)
484 | self.create_table(TABLE_HIGH_LOW)
485 |
486 | # Store Initial Data
487 | storage = (STORAGE_ID, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
488 | self.create_storage_row(storage)
489 | self.initializeHighLow()
490 |
491 | # Update the version number
492 | cursor = self.connection.cursor()
493 | cursor.execute(f"PRAGMA main.user_version = {DATABASE_VERSION};")
494 |
495 | # Migrate data if they exist
496 | if os.path.isfile(STORAGE_FILE):
497 | self.migrateStorageFile()
498 |
499 | except Exception as e:
500 | _LOGGER.error("Could not Read storage file. Error message: %s", e)
501 |
502 | def upgradeDatabase(self):
503 | """Upgrade Database to ensure tables and columns are correct."""
504 | try:
505 | # Get Database Version
506 | cursor = self.connection.cursor()
507 | cursor.execute("PRAGMA main.user_version;")
508 | db_version = int(cursor.fetchone()[0])
509 |
510 | if db_version < 1:
511 | _LOGGER.info("Upgrading the database to version 1")
512 | # Create Empty Tables
513 | self.create_table(TABLE_HIGH_LOW)
514 | # Add Initial data to High Low
515 | self.initializeHighLow()
516 |
517 | if db_version < DATABASE_VERSION:
518 | _LOGGER.info("Upgrading the database to version 2")
519 | cursor.execute("ALTER TABLE high_low ADD max_yday REAL")
520 | cursor.execute("ALTER TABLE high_low ADD max_yday_time REAL")
521 | cursor.execute("ALTER TABLE high_low ADD min_yday REAL")
522 | cursor.execute("ALTER TABLE high_low ADD min_yday_time REAL")
523 |
524 | self.connection.commit()
525 |
526 | # if db_version < 2:
527 | # _LOGGER.info("Upgrading the database to version 2")
528 | # cursor.execute("ALTER TABLE high_low ADD max_yday REAL")
529 | # cursor.execute("ALTER TABLE high_low ADD max_yday_time REAL")
530 | # cursor.execute("ALTER TABLE high_low ADD min_yday REAL")
531 | # cursor.execute("ALTER TABLE high_low ADD min_yday_time REAL")
532 |
533 | # if db_version < DATABASE_VERSION:
534 | # _LOGGER.info("Upgrading the database to version %s...", DATABASE_VERSION)
535 | # self.create_table(TABLE_DAY_DATA)
536 | # self.connection.commit()
537 |
538 | # Finally update the version number
539 | cursor.execute(f"PRAGMA main.user_version = {DATABASE_VERSION};")
540 | _LOGGER.info("Database now version %s", DATABASE_VERSION)
541 |
542 | except Exception as e:
543 | _LOGGER.error("An undefined error occured. Error message: %s", e)
544 |
545 | def initializeHighLow(self):
546 | """Write Initial Data to the High Low Tabble."""
547 | try:
548 | cursor = self.connection.cursor()
549 | cursor.execute(
550 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_DEWPOINT}', -9999, 9999);"
551 | )
552 | cursor.execute(
553 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_HUMIDITY}', -9999, 9999);"
554 | )
555 | cursor.execute(
556 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_ILLUMINANCE}', 0, 0);"
557 | )
558 | cursor.execute(
559 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_PRESSURE}', -9999, 9999);"
560 | )
561 | cursor.execute(
562 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_RAINDURATION}', 0, 0);"
563 | )
564 | cursor.execute(
565 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_RAINRATE}', 0, 0);"
566 | )
567 | cursor.execute(
568 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_SOLARRAD}', 0, 0);"
569 | )
570 | cursor.execute(
571 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_STRIKECOUNT}', 0, 0);"
572 | )
573 | cursor.execute(
574 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_STRIKEENERGY}', 0, 0);"
575 | )
576 | cursor.execute(
577 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_TEMPERATURE}', -9999, 9999);"
578 | )
579 | cursor.execute(
580 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_UV}', 0, 0);"
581 | )
582 | cursor.execute(
583 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_WINDGUST}', 0, 0);"
584 | )
585 | cursor.execute(
586 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_WINDLULL}', 0, 0);"
587 | )
588 | cursor.execute(
589 | f"INSERT INTO high_low(sensorid, max_day, min_day) VALUES('{COL_WINDSPEED}', 0, 0);"
590 | )
591 | self.connection.commit()
592 |
593 | except SQLError as e:
594 | _LOGGER.error("Could not Insert data in table high_low. Error: %s", e)
595 | except Exception as e:
596 | _LOGGER.error("Could write to high_low Table. Error message: %s", e)
597 |
598 | def dailyHousekeeping(self):
599 | """Clean up old data, daily."""
600 | try:
601 | # Cleanup the Pressure Table
602 | pres_time_point = time.time() - PRESSURE_TREND_TIMER - 60
603 | cursor = self.connection.cursor()
604 | cursor.execute(f"DELETE FROM pressure WHERE timestamp < {pres_time_point};")
605 |
606 | # Cleanup the Lightning Table
607 | strike_time_point = time.time() - STRIKE_COUNT_TIMER - 60
608 | cursor.execute(
609 | f"DELETE FROM lightning WHERE timestamp < {strike_time_point};"
610 | )
611 |
612 | # Update All Time Values values
613 | cursor.execute(
614 | f"UPDATE high_low SET max_all = max_day, max_all_time = max_day_time WHERE max_day > max_all or max_all IS NULL"
615 | )
616 | cursor.execute(
617 | f"UPDATE high_low SET min_all = min_day, min_all_time = min_day_time WHERE (min_day < min_all or min_all IS NULL) and min_day_time IS NOT NULL"
618 | )
619 |
620 | # Update or Reset Year Values
621 | cursor.execute(
622 | f"UPDATE high_low SET max_year = max_day, max_year_time = max_day_time WHERE (max_day > max_year or max_year IS NULL) AND strftime('%Y', 'now') = strftime('%Y', datetime(max_day_time, 'unixepoch', 'localtime'))"
623 | )
624 | cursor.execute(
625 | f"UPDATE high_low SET min_year = min_day, min_year_time = min_day_time WHERE ((min_day < min_year or min_year IS NULL) AND min_day_time IS NOT NULL) AND strftime('%Y', 'now') = strftime('%Y', datetime(min_day_time, 'unixepoch', 'localtime'))"
626 | )
627 | cursor.execute(
628 | f"UPDATE high_low SET max_year = latest, max_year_time = {time.time()}, min_year = latest, min_year_time = {time.time()} WHERE min_day <> 0 AND strftime('%Y', 'now') <> strftime('%Y', datetime(max_day_time, 'unixepoch', 'localtime'))"
629 | )
630 | cursor.execute(
631 | f"UPDATE high_low SET max_year = 0, max_year_time = {time.time()} WHERE min_day = 0 AND strftime('%Y', 'now') <> strftime('%Y', datetime(max_day_time, 'unixepoch', 'localtime'))"
632 | )
633 |
634 | # Update or Reset Month Values
635 | cursor.execute(
636 | f"UPDATE high_low SET max_month = max_day, max_month_time = max_day_time WHERE (max_day > max_month or max_month IS NULL) AND strftime('%m', 'now') = strftime('%m', datetime(max_day_time, 'unixepoch', 'localtime'))"
637 | )
638 | cursor.execute(
639 | f"UPDATE high_low SET min_month = min_day, min_month_time = min_day_time WHERE ((min_day < min_month or min_month IS NULL) AND min_day_time IS NOT NULL) AND strftime('%m', 'now') = strftime('%m', datetime(min_day_time, 'unixepoch', 'localtime'))"
640 | )
641 | cursor.execute(
642 | f"UPDATE high_low SET max_month = latest, max_month_time = {time.time()}, min_month = latest, min_month_time = {time.time()} WHERE min_day <> 0 AND strftime('%m', 'now') <> strftime('%m', datetime(max_day_time, 'unixepoch', 'localtime'))"
643 | )
644 | cursor.execute(
645 | f"UPDATE high_low SET max_month = 0, max_month_time = {time.time()} WHERE min_day = 0 AND strftime('%m', 'now') <> strftime('%m', datetime(max_day_time, 'unixepoch', 'localtime'))"
646 | )
647 |
648 | # Update or Reset Week Values
649 | cursor.execute(
650 | f"UPDATE high_low SET max_week = max_day, max_week_time = max_day_time WHERE (max_day > max_week or max_week IS NULL) AND strftime('%W', 'now') = strftime('%W', datetime(max_day_time, 'unixepoch', 'localtime'))"
651 | )
652 | cursor.execute(
653 | f"UPDATE high_low SET min_week = min_day, min_week_time = min_day_time WHERE ((min_day < min_week or min_week IS NULL) AND min_day_time IS NOT NULL) AND strftime('%W', 'now') = strftime('%W', datetime(min_day_time, 'unixepoch', 'localtime'))"
654 | )
655 | cursor.execute(
656 | f"UPDATE high_low SET max_week = latest, max_week_time = {time.time()}, min_week = latest, min_week_time = {time.time()} WHERE min_day <> 0 AND strftime('%W', 'now') <> strftime('%W', datetime(max_day_time, 'unixepoch', 'localtime'))"
657 | )
658 | cursor.execute(
659 | f"UPDATE high_low SET max_week = 0, max_week_time = {time.time()} WHERE min_day = 0 AND strftime('%W', 'now') <> strftime('%W', datetime(max_day_time, 'unixepoch', 'localtime'))"
660 | )
661 |
662 | # Update Yesterday Values
663 | cursor.execute(
664 | f"UPDATE high_low SET max_yday = max_day, max_yday_time = max_day_time, min_yday = min_day, min_yday_time = min_day_time"
665 | )
666 |
667 | # Reset Day High and Low values
668 | cursor.execute(
669 | f"UPDATE high_low SET max_day = latest, max_day_time = {time.time()}, min_day = latest, min_day_time = {time.time()} WHERE min_day <> 0"
670 | )
671 | cursor.execute(
672 | f"UPDATE high_low SET max_day = 0, max_day_time = {time.time()} WHERE min_day = 0"
673 | )
674 | self.connection.commit()
675 |
676 | return True
677 |
678 | except SQLError as e:
679 | _LOGGER.error("Could not perform daily housekeeping. Error: %s", e)
680 | return False
681 | except Exception as e:
682 | _LOGGER.error("Could not perform daily housekeeping. Error message: %s", e)
683 | return False
684 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/translations/da.json:
--------------------------------------------------------------------------------
1 | {
2 | "beaufort": {
3 | "0": "Stille",
4 | "1": "Næsten stille",
5 | "2": "Svag vind",
6 | "3": "Let vind",
7 | "4": "Jævn vind",
8 | "5": "Frisk vind",
9 | "6": "Hård vind",
10 | "7": "Stiv kuling",
11 | "8": "Hård kuling",
12 | "9": "Stormende kuling",
13 | "10": "Storm",
14 | "11": "Stærk storm",
15 | "12": "Orkan"
16 | },
17 | "wind_dir": {
18 | "N": "N",
19 | "NNE": "NNØ",
20 | "NE": "NØ",
21 | "ENE": "ØNØ",
22 | "E": "Ø",
23 | "ESE": "ØSØ",
24 | "SE": "SØ",
25 | "SSE": "SSØ",
26 | "S": "S",
27 | "SSW": "SSV",
28 | "SW": "SV",
29 | "WSW": "VSV",
30 | "W": "V",
31 | "WNW": "VNV",
32 | "NW": "NV",
33 | "NNW": "NNV"
34 | },
35 | "trend": {
36 | "falling": "Falder",
37 | "rising": "Stiger",
38 | "steady": "Stabil"
39 | },
40 | "dewpoint": {
41 | "severely-high": "Meget høj",
42 | "miserable": "Miserabel",
43 | "oppressive": "Tryggende",
44 | "uncomfortable": "Ubehagelig",
45 | "ok-for-most": "Ok for de fleste",
46 | "comfortable": "Behageligt",
47 | "very-comfortable": "Meget behageligt",
48 | "somewhat-dry": "Delvist tørt",
49 | "dry": "Tørt",
50 | "very-dry": "Meget tørt",
51 | "undefined": "Ingen data"
52 | },
53 | "temperature": {
54 | "inferno": "Mega varmt",
55 | "very-hot": "Meget varmt",
56 | "hot": "Varmt",
57 | "warm": "Behagelig varmt",
58 | "nice": "Dejligt",
59 | "cool": "Kølig",
60 | "chilly": "Meget køligt",
61 | "cold": "Koldt",
62 | "freezing": "Meget koldt",
63 | "fridged": "Frysende",
64 | "undefined": "Ingen data"
65 | },
66 | "uv": {
67 | "extreme": "Ekstrem høj",
68 | "very-high": "Meget høj",
69 | "high": "Høj",
70 | "moderate": "Moderat",
71 | "low": "Lav",
72 | "none": "Ingen data"
73 | },
74 | "precip_type": {
75 | "none": "Ingen",
76 | "rain": "Regn",
77 | "hail": "Hagl",
78 | "heavy-rain": "Kraftig regn"
79 | },
80 | "rain_intensity": {
81 | "NONE": "Ingen",
82 | "VERYLIGHT": "Meget Let",
83 | "LIGHT": "Let",
84 | "MODERATE": "Moderat",
85 | "HEAVY": "Kraftig",
86 | "VERYHEAVY": "Meget Kraftig",
87 | "EXTREME": "Ekstrem"
88 | },
89 | "zambretti": {
90 | "A": "Stabilt fint vejr",
91 | "B": "Fint vejr",
92 | "C": "Opklarende, bliver fint",
93 | "D": "Fint. Bliver mere ustabilt",
94 | "E": "Fint, med mulighed for lette byger",
95 | "F": "Opholdsvejr. Bedre vejr senere",
96 | "G": "Opholdsvejr. Mulighed for lette byger senere",
97 | "H": "Opholdsvejr. Regn senere",
98 | "I": "Tidlig lette regnbyer, bedre senere",
99 | "J": "Vekslende vejr, bliver bedre",
100 | "K": "Rimeligt fint vejr, mulighed for byger senere",
101 | "L": "Temmelig ustabilt vejr, klarer op senere",
102 | "M": "Ustabilt, bedres formentlig",
103 | "N": "Byger, med jævne intervaller",
104 | "O": "Byger, bliver mere ustabilt",
105 | "P": "Foranderligt, nogen regn",
106 | "Q": "Byger, med små korte intervaller",
107 | "R": "Ustabilt, regn senere",
108 | "S": "Ustabilt, regn med jævne mellemrum",
109 | "T": "Meget ustabilt, men mulighed for opholdsvejr",
110 | "U": "Regnbyger, bliver mere ustabilt",
111 | "V": "Regnbyer, bliver meget ustabilt",
112 | "W": "Regn med jævne mellemrum",
113 | "X": "Meget ustabilt, regn",
114 | "Y": "Storm, bliver muligvis bedre",
115 | "Z": "Storm, megen regn"
116 | }
117 | }
--------------------------------------------------------------------------------
/weatherflow2mqtt/translations/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "beaufort": {
3 | "0": "Windstille",
4 | "1": "Leiser Zug",
5 | "2": "Leichte Brise",
6 | "3": "Schwache Brise",
7 | "4": "Mäßige Brise",
8 | "5": "Frische Brise",
9 | "6": "Starker Wind",
10 | "7": "Steifer Wind",
11 | "8": "Stürmischer Wind",
12 | "9": "Sturm",
13 | "10": "Schwerer Sturm",
14 | "11": "Orkanartiger Sturm",
15 | "12": "Orkan"
16 | },
17 | "wind_dir": {
18 | "N": "N",
19 | "NNE": "NNO",
20 | "NE": "NO",
21 | "ENE": "ONO",
22 | "E": "O",
23 | "ESE": "OSO",
24 | "SE": "SO",
25 | "SSE": "SSO",
26 | "S": "S",
27 | "SSW": "SSW",
28 | "SW": "SW",
29 | "WSW": "WSW",
30 | "W": "W",
31 | "WNW": "WNW",
32 | "NW": "NW",
33 | "NNW": "NNW"
34 | },
35 | "trend": {
36 | "falling": "Fallend",
37 | "rising": "Steigend",
38 | "steady": "Stabil"
39 | },
40 | "dewpoint": {
41 | "severely-high": "Ernsthaft hoch",
42 | "miserable": "Miserabel",
43 | "oppressive": "Drückend",
44 | "uncomfortable": "Unangenehm",
45 | "ok-for-most": "Großteils okay",
46 | "comfortable": "Angenehm",
47 | "very-comfortable": "Sehr angenehm",
48 | "somewhat-dry": "Etwas trocken",
49 | "dry": "Trocken",
50 | "very-dry": "Sehr trocken",
51 | "undefined": "Undefiniert"
52 | },
53 | "temperature": {
54 | "inferno": "Inferno",
55 | "very-hot": "Sehr heiß",
56 | "hot": "Heiß",
57 | "warm": "Warm",
58 | "nice": "Angenehm",
59 | "cool": "Frisch",
60 | "chilly": "Kühl",
61 | "cold": "Kalt",
62 | "freezing": "Eiskalt",
63 | "fridged": "Gefroren",
64 | "undefined": "Undefiniert"
65 | },
66 | "uv": {
67 | "extreme": "Extrem",
68 | "very-high": "Sehr hoch",
69 | "high": "Hoch",
70 | "moderate": "Mäßig",
71 | "low": "Niedrig",
72 | "none": "Keines"
73 | },
74 | "precip_type": {
75 | "none": "Keiner",
76 | "rain": "Regen",
77 | "hail": "Hagel",
78 | "heavy-rain": "Starkregen"
79 | },
80 | "rain_intensity": {
81 | "NONE": "Keiner",
82 | "VERYLIGHT": "Sehr Leicht",
83 | "LIGHT": "Leicht",
84 | "MODERATE": "Mäßig",
85 | "HEAVY": "Schwer",
86 | "VERYHEAVY": "Seht Schwer",
87 | "EXTREME": "Extrem"
88 | },
89 | "zambretti": {
90 | "A": "Beständiges Schönwetter",
91 | "B": "Schönes Wetter",
92 | "C": "Wetter wird gut",
93 | "D": "Schön, wird wechselhaft",
94 | "E": "Schön, Regenschauer möglich",
95 | "F": "Ziemlich gut, verbessert sich",
96 | "G": "Ziemlich gut, frühe Regenschauer möglich",
97 | "H": "Ziemlich gut, spätere Regenschauer",
98 | "I": "Früh schauerhaft, verbessert sich",
99 | "J": "Wechselhaft, verbessert sich",
100 | "K": "Ziemlich gut, Regenschauer möglich",
101 | "L": "Eher veränderlich, klart später auf",
102 | "M": "Veränderlich, verbessert sich wahrscheinlich",
103 | "N": "Regnerisch mit Aufhellungen",
104 | "O": "Regnerisch, wird veränderlich",
105 | "P": "Veränderlich mit wenig Regen",
106 | "Q": "Veränderlich, mit kurzen schönen Intervallen",
107 | "R": "Veränderlich, später Regen",
108 | "S": "Veränderlich, zeitweise Regen",
109 | "T": "Stark wechselnd, zeitweise schöner",
110 | "U": "Zeitweise Regen, verschlechtert sich",
111 | "V": "Zeitweise Regen, wird sehr unruhig",
112 | "W": "Regen in regelmässigen Abständen",
113 | "X": "Sehr veränderlich, Regen",
114 | "Y": "Stürmisch, verbessert sich wahrscheinlich",
115 | "Z": "Stürmisch, viel Regen"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "beaufort": {
3 | "0": "Calm",
4 | "1": "Light air",
5 | "2": "Light breeze",
6 | "3": "Gentle breeze",
7 | "4": "Moderate breeze",
8 | "5": "Fresh breeze",
9 | "6": "Strong breeze",
10 | "7": "Moderate Gale",
11 | "8": "Fresh Gale",
12 | "9": "Strong Gale",
13 | "10": "Storm",
14 | "11": "Violent Storm",
15 | "12": "Hurricane"
16 | },
17 | "wind_dir": {
18 | "N": "N",
19 | "NNE": "NNE",
20 | "NE": "NE",
21 | "ENE": "ENE",
22 | "E": "E",
23 | "ESE": "ESE",
24 | "SE": "SE",
25 | "SSE": "SSE",
26 | "S": "S",
27 | "SSW": "SSW",
28 | "SW": "SW",
29 | "WSW": "WSW",
30 | "W": "W",
31 | "WNW": "WNW",
32 | "NW": "NW",
33 | "NNW": "NNW"
34 | },
35 | "trend": {
36 | "falling": "Falling",
37 | "rising": "Rising",
38 | "steady": "Steady"
39 | },
40 | "dewpoint": {
41 | "severely-high": "Severely High",
42 | "miserable": "Miserable",
43 | "oppressive": "Oppressive",
44 | "uncomfortable": "Uncomfortable",
45 | "ok-for-most": "Ok for Most",
46 | "comfortable": "Comfortable",
47 | "very-comfortable": "Very Comfortable",
48 | "somewhat-dry": "Somewhat Dry",
49 | "dry": "Dry",
50 | "very-dry": "Very Dry",
51 | "undefined": "Undefined"
52 | },
53 | "temperature": {
54 | "inferno": "Inferno",
55 | "very-hot": "Very Hot",
56 | "hot": "Hot",
57 | "warm": "Warm",
58 | "nice": "Nice",
59 | "cool": "Cool",
60 | "chilly": "Chilly",
61 | "cold": "Cold",
62 | "freezing": "Freezing",
63 | "fridged": "Frigid",
64 | "undefined": "Undefined"
65 | },
66 | "uv": {
67 | "extreme": "Extreme",
68 | "very-high": "Very High",
69 | "high": "High",
70 | "moderate": "Moderate",
71 | "low": "Low",
72 | "none": "None"
73 | },
74 | "precip_type": {
75 | "none": "None",
76 | "rain": "Rain",
77 | "hail": "Hail",
78 | "heavy-rain": "Heavy Rain"
79 | },
80 | "rain_intensity": {
81 | "NONE": "None",
82 | "VERYLIGHT": "Very Light",
83 | "LIGHT": "Light",
84 | "MODERATE": "Moderate",
85 | "HEAVY": "Heavy",
86 | "VERYHEAVY": "Very Heavy",
87 | "EXTREME": "Extreme"
88 | },
89 | "zambretti": {
90 | "A": "Settled fine weather",
91 | "B": "Fine weather",
92 | "C": "Becoming fine",
93 | "D": "Fine, becoming less settled",
94 | "E": "Fine, possibly showers",
95 | "F": "Fairly fine, improving",
96 | "G": "Fairly fine, possibly showers early",
97 | "H": "Fairly fine, showers later",
98 | "I": "Showery early, improving",
99 | "J": "Changeable, improving",
100 | "K": "Fairly fine, showers likely",
101 | "L": "Rather unsettled, clearing later",
102 | "M": "Unsettled, probably improving",
103 | "N": "Showery, bright intervals",
104 | "O": "Showery, becoming unsettled",
105 | "P": "Changeable, some rain",
106 | "Q": "Unsettled, short fine intervals",
107 | "R": "Unsettled, rain later",
108 | "S": "Unsettled, rain at times",
109 | "T": "Very unsettled, finer at times",
110 | "U": "Rain at times, worse later",
111 | "V": "Rain at times, becoming very unsettled",
112 | "W": "Rain at frequent intervals",
113 | "X": "Very unsettled, rain",
114 | "Y": "Stormy, possibly improving",
115 | "Z": "Stormy, much rain"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/translations/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "beaufort": {
3 | "0": "Calme",
4 | "1": "Léger",
5 | "2": "Brise légère",
6 | "3": "Brise légère",
7 | "4": "Brise moyenne",
8 | "5": "Brise fraîche",
9 | "6": "Brise Forte",
10 | "7": "Vent modéré",
11 | "8": "Vent frais",
12 | "9": "Vent fort",
13 | "10": "Tempête",
14 | "11": "Violente tempête",
15 | "12": "Ouragan"
16 | },
17 | "wind_dir": {
18 | "N": "N",
19 | "NNE": "NNE",
20 | "NE": "NE",
21 | "ENE": "ENE",
22 | "E": "E",
23 | "ESE": "ESE",
24 | "SE": "SE",
25 | "SSE": "SSE",
26 | "S": "S",
27 | "SSW": "SSO",
28 | "SW": "SO",
29 | "WSW": "OSO",
30 | "W": "O",
31 | "WNW": "ONO",
32 | "NW": "NO",
33 | "NNW": "NNO"
34 | },
35 | "trend": {
36 | "falling": "En baisse",
37 | "rising": "En hausse",
38 | "steady": "Stable"
39 | },
40 | "dewpoint": {
41 | "severely-high": "Sévèrement élevé",
42 | "miserable": "Misérable",
43 | "oppressive": "Oppressif",
44 | "uncomfortable": "Inconfortable",
45 | "ok-for-most": "Ok pour la plupart",
46 | "comfortable": "Comfortable",
47 | "very-comfortable": "Très confortable",
48 | "somewhat-dry": "Un peu sec",
49 | "dry": "Sec",
50 | "very-dry": "Très sec",
51 | "undefined": "Non défini"
52 | },
53 | "temperature": {
54 | "inferno": "D’enfer",
55 | "very-hot": "Très chaude",
56 | "hot": "Chaude",
57 | "warm": "Chaleureuse",
58 | "nice": "Agréable",
59 | "cool": "Fraiche",
60 | "chilly": "Froide",
61 | "cold": "Froide",
62 | "freezing": "Gel",
63 | "fridged": "Réfrigéré",
64 | "undefined": "Non définie"
65 | },
66 | "uv": {
67 | "extreme": "Extrême",
68 | "very-high": "Très haut",
69 | "high": "Haut",
70 | "moderate": "Moyen",
71 | "low": "Bas",
72 | "none": "Aucun"
73 | },
74 | "precip_type": {
75 | "none": "Aucune",
76 | "rain": "Pluie",
77 | "hail": "Grêle",
78 | "heavy-rain": "Forte pluie"
79 | },
80 | "rain_intensity": {
81 | "NONE": "Aucune",
82 | "VERYLIGHT": "Très faible",
83 | "LIGHT": "Faible",
84 | "MODERATE": "Modérée",
85 | "HEAVY": "Forte",
86 | "VERYHEAVY": "Très forte",
87 | "EXTREME": "Extrême"
88 | },
89 | "zambretti": {
90 | "A": "Beau temps installé",
91 | "B": "Beau temps",
92 | "C": "Amélioration de la météo",
93 | "D": "Beau temps, deviens changeant",
94 | "E": "Beau temps, Possibilité d'averses",
95 | "F": "Assez bon, en amélioration",
96 | "G": "Assez bon, des averses précoces sont possibles",
97 | "H": "Assez bon, des averses plus tard",
98 | "I": "Pluie précoce, amélioration",
99 | "J": "Changeant s'améliore",
100 | "K": "Assez bon, averses probables",
101 | "L": "Plutôt variable, s'éclaircira plus tard",
102 | "M": "Instable, probablement en amélioration",
103 | "N": "Pluie avec éclaircissement",
104 | "O": "Pluvieux, devient instable",
105 | "P": "Variable avec peu d'averses",
106 | "Q": "Instable, avec quelques intervalles de beau",
107 | "R": "Instable, averses plus tard",
108 | "S": "Instable, averses occasionnelles",
109 | "T": "Très Instable, parfois plus beau",
110 | "U": "Averses par moments, plus mauvais plus tard",
111 | "V": "Averses par moments, devient très instable",
112 | "W": "Averses à intervalles fréquents",
113 | "X": "Très instable, Pluie",
114 | "Y": "Orageux, possible amélioration",
115 | "Z": "Orageux, beaucoup de pluie"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/translations/nl.json:
--------------------------------------------------------------------------------
1 | {
2 | "beaufort": {
3 | "0": "Stil",
4 | "1": "Zeer zwak",
5 | "2": "Zwak",
6 | "3": "Vrij matig",
7 | "4": "Matig",
8 | "5": "Vrij krachtig",
9 | "6": "Krachtig",
10 | "7": "Hard",
11 | "8": "Stormachtig",
12 | "9": "Storm",
13 | "10": "Zware storm",
14 | "11": "Zeer zware storm",
15 | "12": "Orkaan"
16 | },
17 | "wind_dir": {
18 | "N": "N",
19 | "NNE": "NNO",
20 | "NE": "NO",
21 | "ENE": "ONO",
22 | "E": "O",
23 | "ESE": "OZO",
24 | "SE": "ZO",
25 | "SSE": "ZZO",
26 | "S": "Z",
27 | "SSW": "ZZW",
28 | "SW": "ZW",
29 | "WSW": "WZW",
30 | "W": "W",
31 | "WNW": "WNW",
32 | "NW": "NW",
33 | "NNW": "NNW"
34 | },
35 | "trend": {
36 | "falling": "Dalend",
37 | "rising": "Stijgend",
38 | "steady": "Stabiel"
39 | },
40 | "dewpoint": {
41 | "severely-high": "Gevaarlijk hoog",
42 | "miserable": "Ellendig",
43 | "oppressive": "Drukkend",
44 | "uncomfortable": "Ongemakkelijk",
45 | "ok-for-most": "Redelijk goed",
46 | "comfortable": "Comfortabel",
47 | "very-comfortable": "Zeer comfortabel",
48 | "somewhat-dry": "Licht droog",
49 | "dry": "Droog",
50 | "very-dry": "Zeer droog",
51 | "undefined": "Ongedefinieerd"
52 | },
53 | "temperature": {
54 | "inferno": "Extreem heet",
55 | "very-hot": "Erg heet",
56 | "hot": "Heet",
57 | "warm": "Warm",
58 | "nice": "Lekker",
59 | "cool": "Koel",
60 | "chilly": "Fris",
61 | "cold": "Koud",
62 | "freezing": "IJskoud",
63 | "fridged": "Vrieskou",
64 | "undefined": "Ongedefinieerd"
65 | },
66 | "uv": {
67 | "extreme": "Extreem",
68 | "very-high": "Erg hoog",
69 | "high": "Hoog",
70 | "moderate": "Gemiddeld",
71 | "low": "Laag",
72 | "none": "Geen"
73 | },
74 | "precip_type": {
75 | "none": "Geen",
76 | "rain": "Regen",
77 | "hail": "Hagel",
78 | "heavy-rain": "Zware regen"
79 | },
80 | "rain_intensity": {
81 | "NONE": "Geen",
82 | "VERYLIGHT": "Erg licht",
83 | "LIGHT": "Licht",
84 | "MODERATE": "Gemiddeld",
85 | "HEAVY": "Zwaar",
86 | "VERYHEAVY": "Zeer zwaar",
87 | "EXTREME": "Extreem"
88 | },
89 | "zambretti": {
90 | "A": "Stabiel mooi weer",
91 | "B": "Mooi weer",
92 | "C": "Aankomend mooi weer",
93 | "D": "Mooi, het wordt minder stabiel",
94 | "E": "Mooi, mogelijk regen",
95 | "F": "Goed, verbeterend",
96 | "G": "Goed, mogelijk regen",
97 | "H": "Goed, mogelijk later regen",
98 | "I": "Regenachtig, verbeterend",
99 | "J": "Veranderlijk, verbeterend",
100 | "K": "Redelijk goed, mogelijk regen",
101 | "L": "Redelijk onstabiel, later mooier",
102 | "M": "Onstabiel, mogelijk verbeterend",
103 | "N": "Regenachtig met heldere opklaringen",
104 | "O": "Regenachtig, het wordt onstabiel",
105 | "P": "Veranderlijk, soms regen",
106 | "Q": "Onstabiel met korte opklaringen",
107 | "R": "Onstabiel, later regen",
108 | "S": "Onveranderlijk, geregeld regen",
109 | "T": "Zeer onstabiel, af en toe beter",
110 | "U": "Geregeld regen, later slechter",
111 | "V": "Geregeld regen, het wordt onstabiel",
112 | "W": "Geregeld regen",
113 | "X": "Erg onstabiel, regen",
114 | "Y": "Stormachtig, mogelijk verbeterend",
115 | "Z": "Stormachtig, veel regen"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/translations/se.json:
--------------------------------------------------------------------------------
1 | {
2 | "beaufort": {
3 | "0": "Stiltje",
4 | "1": "Nästan stiltje",
5 | "2": "Lätt bris",
6 | "3": "God bris",
7 | "4": "Frisk bris",
8 | "5": "Styv bris",
9 | "6": "Hård bris",
10 | "7": "Styv kuling",
11 | "8": "Hård kuling",
12 | "9": "Halv storm",
13 | "10": "Storm",
14 | "11": "Svår storm",
15 | "12": "Orkan"
16 | },
17 | "wind_dir": {
18 | "N": "N",
19 | "NNE": "NNÖ",
20 | "NE": "NÖ",
21 | "ENE": "ÖNÖ",
22 | "E": "Ö",
23 | "ESE": "ÖSÖ",
24 | "SE": "SÖ",
25 | "SSE": "SSÖ",
26 | "S": "S",
27 | "SSW": "SSV",
28 | "SW": "SV",
29 | "WSW": "VSV",
30 | "W": "V",
31 | "WNW": "VNV",
32 | "NW": "NV",
33 | "NNW": "NNV"
34 | },
35 | "trend": {
36 | "falling": "Fallande",
37 | "rising": "Stigande",
38 | "steady": "Stadig"
39 | },
40 | "dewpoint": {
41 | "severely-high": "Mycket hög",
42 | "miserable": "Miserabel",
43 | "oppressive": "Tryckande",
44 | "uncomfortable": "Obehaglig",
45 | "ok-for-most": "Ok för de flesta",
46 | "comfortable": "Behaglig",
47 | "very-comfortable": "Väldigt behaglig",
48 | "somewhat-dry": "Delvis torr",
49 | "dry": "Torr",
50 | "very-dry": "Väldigt torr",
51 | "undefined": "Ingen data"
52 | },
53 | "temperature": {
54 | "inferno": "Extremt varmt",
55 | "very-hot": "Mycket varmt",
56 | "hot": "Varmt",
57 | "warm": "Behagligt varmt",
58 | "nice": "Fint",
59 | "cool": "Kyligt",
60 | "chilly": "Mycket kyligt",
61 | "cold": "Kallt",
62 | "freezing": "Mycket kallt",
63 | "fridged": "Extremt kallt",
64 | "undefined": "Ingen data"
65 | },
66 | "uv": {
67 | "extreme": "Extremt hög",
68 | "very-high": "Mycket hög",
69 | "high": "Hög",
70 | "moderate": "Moderat",
71 | "low": "Låg",
72 | "none": "Ingen"
73 | },
74 | "precip_type": {
75 | "none": "Ingen",
76 | "rain": "Regn",
77 | "hail": "Hagel",
78 | "heavy-rain": "Ösregn"
79 | },
80 | "rain_intensity": {
81 | "NONE": "Uppehåll",
82 | "VERYLIGHT": "Mycket lätt",
83 | "LIGHT": "Lätt",
84 | "MODERATE": "Moderat",
85 | "HEAVY": "Kraftig",
86 | "VERYHEAVY": "Väldigt kraftig",
87 | "EXTREME": "Extrem"
88 | },
89 | "zambretti": {
90 | "A": "Stadigt fint väder",
91 | "B": "Fint väder",
92 | "C": "Uppklarnande, blir fint",
93 | "D": "Fint, blir mer ostadigt",
94 | "E": "Fint, med risk för lätta skurar",
95 | "F": "Uppehåll, bättre senare",
96 | "G": "Uppehåll, risk för lätta skurar senare",
97 | "H": "Uppehåll, regn senare",
98 | "I": "Tidigt lätta skurar, bättre senare",
99 | "J": "Växlande, bättre senare",
100 | "K": "Ganska fint, risk för lätta skurar senare",
101 | "L": "Ganska ostadigt, klarnar upp senare",
102 | "M": "Ostabilt, troligen bättre senare",
103 | "N": "Regnbyar, med jämna mellanrum",
104 | "O": "Regnbyar, blir mer ostadigt",
105 | "P": "Omväxlande, lite regn",
106 | "Q": "Ostadigt, regnbyar",
107 | "R": "Ostadigt, regn senare",
108 | "S": "Ostadigt, regn med jämna mellanrum",
109 | "T": "Mycket ostadigt, men möjlighet till uppehållsväder",
110 | "U": "Regnbyar, sämre senare",
111 | "V": "Regnbyar, blir mer ostadigt",
112 | "W": "Regn med jämna mellanrum",
113 | "X": "Mycket ostabilt, regn",
114 | "Y": "Storm, möjlig förbättring",
115 | "Z": "Storm, mycket regn"
116 | }
117 | }
--------------------------------------------------------------------------------
/weatherflow2mqtt/translations/si.json:
--------------------------------------------------------------------------------
1 | {
2 | "beaufort": {
3 | "0": "Tišina",
4 | "1": "Lahen vetrič",
5 | "2": "Vetrič",
6 | "3": "Slab veter",
7 | "4": "Zmeren veter",
8 | "5": "Zmerno močan veter",
9 | "6": "Močan veter",
10 | "7": "Zelo močan veter",
11 | "8": "Viharni veter",
12 | "9": "Vihar",
13 | "10": "Močan vihar",
14 | "11": "Orkanski veter",
15 | "12": "Orkan"
16 | },
17 | "wind_dir": {
18 | "N": "Sever",
19 | "NNE": "Sever-severovzhod",
20 | "NE": "Severovzhod",
21 | "ENE": "Vzhod-Severovzhod",
22 | "E": "Vzhod",
23 | "ESE": "Vzhod-jugovzhod",
24 | "SE": "Jugovzhod",
25 | "SSE": "Jug-jugovzhod",
26 | "S": "Jug",
27 | "SSW": "Jug-jugozahod",
28 | "SW": "Jugozahod",
29 | "WSW": "Zahod-jugozahod",
30 | "W": "Zahod",
31 | "WNW": "Zahod-severozahod",
32 | "NW": "Severozahod",
33 | "NNW": "Sever-severozahod"
34 | },
35 | "trend": {
36 | "falling": "Padajoče",
37 | "rising": "Naraščajoče",
38 | "steady": "Stalno"
39 | },
40 | "dewpoint": {
41 | "severely-high": "Izjemno visoko",
42 | "miserable": "Neznosno",
43 | "oppressive": "Zatohlo",
44 | "uncomfortable": "Neprijetno",
45 | "ok-for-most": "Za večino v redu",
46 | "comfortable": "Prijetno",
47 | "very-comfortable": "Zelo prijetno",
48 | "somewhat-dry": "Precej suho",
49 | "dry": "Suho",
50 | "very-dry": "Zelo suho",
51 | "undefined": "Nedoločeno"
52 | },
53 | "temperature": {
54 | "inferno": "Pekel",
55 | "very-hot": "Zelo vroče",
56 | "hot": "Vroče",
57 | "warm": "Toplo",
58 | "nice": "Prijetno",
59 | "cool": "Ohlajeno",
60 | "chilly": "Hladno",
61 | "cold": "Mrzlo",
62 | "freezing": "Zelo mrzlo",
63 | "fridged": "Ledeno",
64 | "undefined": "Nedoločeno"
65 | },
66 | "uv": {
67 | "extreme": "Ekstremno",
68 | "very-high": "Zelo visoko",
69 | "high": "Visoko",
70 | "moderate": "Zmerno",
71 | "low": "Nizko",
72 | "none": "Brez"
73 | },
74 | "precip_type": {
75 | "none": "Brez padavin",
76 | "rain": "Dež",
77 | "hail": "Toča",
78 | "heavy-rain": "Naliv"
79 | },
80 | "rain_intensity": {
81 | "NONE": "Brez padavin",
82 | "VERYLIGHT": "Zelo rahelo",
83 | "LIGHT": "Rahlo",
84 | "MODERATE": "Zmerno",
85 | "HEAVY": "Močno",
86 | "VERYHEAVY": "Zelo močno",
87 | "EXTREME": "Ekstremno"
88 | },
89 | "zambretti": {
90 | "A": "Stanovitno lepo vreme",
91 | "B": "Lepo vreme",
92 | "C": "Postaja lepo",
93 | "D": "Lepo, a manj stanovitno",
94 | "E": "Lepo, možni so kratki nalivi",
95 | "F": "Dokaj lepo, se izboljšuje",
96 | "G": "Dokaj lepo, z možnostjo zgodnjih nalivov",
97 | "H": "Dokaj lepo, nalivi kasneje",
98 | "I": "Deževno zgodaj, se izboljšuje",
99 | "J": "Spremenljivo, se izboljšuje",
100 | "K": "Dokaj lepo, verjetnost nalivov",
101 | "L": "Precej nestanovitno, zjasnitev kasneje",
102 | "M": "Nestanovitno, verjetno se izboljšuje",
103 | "N": "Deževno z občasnimi zjasnitvami",
104 | "O": "Deževno, postaja nestanovitno",
105 | "P": "Spremenljivo, nekaj dežja",
106 | "Q": "Nestanovitno, kratka obdobja lepega vremena",
107 | "R": "Nestanovitno, dež pozneje",
108 | "S": "Nestanovitno, občasno deževno",
109 | "T": "Zelo nestanovitno, občasno lepo",
110 | "U": "Občasno deževno, kasneje slabše",
111 | "V": "Občasno deževno, postaja zelo nestanovitno",
112 | "W": "Pogosti intervali dežja",
113 | "X": "Zelo nestanovitno, dež",
114 | "Y": "Nevihta, možno izboljšanje",
115 | "Z": "Nevihta, veliko dežja"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/weatherflow2mqtt/weatherflow_mqtt.py:
--------------------------------------------------------------------------------
1 | """Program listening to the UDP Broadcast.
2 |
3 | from a WeatherFlow Weather Station and publishing sensor data to MQTT.
4 | """
5 | from __future__ import annotations
6 |
7 | import asyncio
8 | import json
9 | import logging
10 | import os
11 | import sys
12 | from dataclasses import dataclass
13 | from datetime import datetime
14 | from math import ceil
15 | from typing import Any, Callable, OrderedDict
16 |
17 | from paho.mqtt.client import Client as MqttClient
18 | from pint import Quantity
19 | from pyweatherflowudp.client import EVENT_DEVICE_DISCOVERED, WeatherFlowListener
20 | from pyweatherflowudp.const import UNIT_METERS
21 | from pyweatherflowudp.device import (
22 | EVENT_LOAD_COMPLETE,
23 | EVENT_OBSERVATION,
24 | EVENT_RAIN_START,
25 | EVENT_RAPID_WIND,
26 | EVENT_STATUS_UPDATE,
27 | EVENT_STRIKE,
28 | AirSensorType,
29 | HubDevice,
30 | SkySensorType,
31 | TempestDevice,
32 | WeatherFlowDevice,
33 | WeatherFlowSensorDevice,
34 | )
35 | from pyweatherflowudp.event import (
36 | CustomEvent,
37 | LightningStrikeEvent,
38 | RainStartEvent,
39 | WindEvent,
40 | )
41 |
42 | from .__version__ import VERSION
43 | from .const import (
44 | ATTR_ATTRIBUTION,
45 | ATTRIBUTION,
46 | DATABASE,
47 | DEVICE_CLASS_TIMESTAMP,
48 | DOMAIN,
49 | EVENT_HIGH_LOW,
50 | EXTERNAL_DIRECTORY,
51 | FORECAST_ENTITY,
52 | HIGH_LOW_TIMER,
53 | LANGUAGE_ENGLISH,
54 | MANUFACTURER,
55 | TEMP_CELSIUS,
56 | UNITS_IMPERIAL,
57 | UNITS_METRIC,
58 | ZAMBRETTI_MAX_PRESSURE,
59 | ZAMBRETTI_MIN_PRESSURE,
60 | )
61 | from .forecast import Forecast, ForecastConfig
62 | from .helpers import ConversionFunctions, read_config, truebool
63 | from .sensor_description import (
64 | DEVICE_SENSORS,
65 | FORECAST_SENSORS,
66 | HUB_SENSORS,
67 | OBSOLETE_SENSORS,
68 | BaseSensorDescription,
69 | SensorDescription,
70 | SqlSensorDescription,
71 | StorageSensorDescription,
72 | )
73 | from .sqlite import SQLFunctions
74 |
75 | _LOGGER = logging.getLogger(__name__)
76 |
77 | MQTT_TOPIC_FORMAT = "homeassistant/sensor/{}/{}/{}"
78 | DEVICE_SERIAL_FORMAT = f"{DOMAIN}_{{}}"
79 |
80 |
81 | @dataclass
82 | class HostPortConfig:
83 | """Dataclass to define a Host/Port configuration."""
84 |
85 | host: str
86 | port: int
87 |
88 |
89 | @dataclass
90 | class MqttConfig(HostPortConfig):
91 | """Dataclass to define a MQTT configuration."""
92 |
93 | host: str = "127.0.0.1"
94 | port: int = 1883
95 | username: str | None = None
96 | password: str | None = None
97 | debug: bool = False
98 |
99 |
100 | @dataclass
101 | class WeatherFlowUdpConfig(HostPortConfig):
102 | """Dataclass to define a UDP configuration."""
103 |
104 | host: str = "0.0.0.0"
105 | port: int = 50222
106 |
107 |
108 | class WeatherFlowMqtt:
109 | """Class to handle WeatherFlow to MQTT communication."""
110 |
111 | def __init__(
112 | self,
113 | elevation: float = 0,
114 | latitude: float = 0,
115 | longitude: float = 0,
116 | unit_system: str = UNITS_METRIC,
117 | rapid_wind_interval: int = 0,
118 | language: str = LANGUAGE_ENGLISH,
119 | mqtt_config: MqttConfig = MqttConfig(),
120 | udp_config: WeatherFlowUdpConfig = WeatherFlowUdpConfig(),
121 | forecast_config: ForecastConfig = None,
122 | database_file: str = None,
123 | filter_sensors: list[str] | None = None,
124 | invert_filter: bool = False,
125 | zambretti_min_pressure = ZAMBRETTI_MIN_PRESSURE,
126 | zambretti_max_pressure = ZAMBRETTI_MAX_PRESSURE
127 | ) -> None:
128 | """Initialize a WeatherFlow MQTT."""
129 | self.elevation = elevation
130 | self.latitude = latitude
131 | self.longitude = longitude
132 | self.unit_system = unit_system
133 | self.rapid_wind_interval = rapid_wind_interval
134 | self.sealevel_pressure_all_high = zambretti_max_pressure
135 | self.sealevel_pressure_all_low = zambretti_min_pressure
136 |
137 | self.cnv = ConversionFunctions(unit_system, language)
138 |
139 | self.mqtt_config = mqtt_config
140 | self.udp_config = udp_config
141 |
142 | self.forecast = (
143 | Forecast.from_config(config=forecast_config, conversions=self.cnv)
144 | if forecast_config is not None
145 | else None
146 | )
147 |
148 | self.mqtt_client: MqttClient = None
149 | self.listener: WeatherFlowListener | None = None
150 | self._queue: asyncio.Queue | None = None
151 | self._queue_task: asyncio.Task | None = None
152 | self._init_sql_db(database_file=database_file)
153 |
154 | self._filter_sensors = filter_sensors
155 | self._invert_filter = invert_filter
156 |
157 | # Set timer variables
158 | self._forecast_next_run: float = 0
159 | self.rapid_last_run = 1621229580.583215 # A time in the past
160 | self.high_low_last_run = 1621229580.583215 # A time in the past
161 | self.current_day = datetime.today().weekday()
162 | self.last_midnight = self.cnv.utc_last_midnight()
163 |
164 | # Read stored Values and set variable values
165 | self.fog_probability = None
166 | self.wind_speed = None
167 | self.snow_probability = None
168 | self.solar_radiation = None
169 | self.solar_elevation = None
170 | self.solar_insolation = None
171 |
172 |
173 | # Zambretti Forecast Vars
174 | self.wind_bearing_avg = None
175 | self.sealevel_pressure = None
176 | self.pressure_trend = None
177 | self.zambretti_number = None
178 |
179 | @property
180 | def is_imperial(self) -> bool:
181 | """Return `True` if the unit system is imperial, else `False`."""
182 | return self.unit_system == UNITS_IMPERIAL
183 |
184 | async def connect(self) -> None:
185 | """Connect to MQTT and UDP."""
186 | if self.mqtt_client is None:
187 | self._setup_mqtt_client()
188 |
189 | try:
190 | self.mqtt_client.connect(
191 | self.mqtt_config.host, port=self.mqtt_config.port, keepalive=300
192 | )
193 | self.mqtt_client.loop_start()
194 | _LOGGER.info(
195 | "Connected to the MQTT server at %s:%s",
196 | self.mqtt_config.host,
197 | self.mqtt_config.port,
198 | )
199 | except Exception as e:
200 | _LOGGER.error("Could not connect to MQTT Server. Error is: %s", e)
201 | sys.exit(1)
202 |
203 | self.listener = WeatherFlowListener(self.udp_config.host, self.udp_config.port)
204 | self.listener.on(
205 | EVENT_DEVICE_DISCOVERED, lambda device: self._device_discovered(device)
206 | )
207 | try:
208 | await self.listener.start_listening()
209 | _LOGGER.info("The UDP server is listening on port %s", self.udp_config.port)
210 | except Exception as e:
211 | _LOGGER.error(
212 | "Could not start listening to the UDP Socket. Error is: %s", e
213 | )
214 | sys.exit(1)
215 |
216 | self._queue = asyncio.Queue()
217 | self._queue_task = asyncio.ensure_future(self._mqtt_queue_processor())
218 |
219 | async def run_time_based_updates(self) -> None:
220 | """Run some time based updates."""
221 | # Run New day function if Midnight
222 | if self.current_day != datetime.today().weekday():
223 | self.storage["rain_yesterday"] = self.storage["rain_today"]
224 | self.storage["rain_duration_yesterday"] = self.storage[
225 | "rain_duration_today"
226 | ]
227 | self.storage["rain_today"] = 0
228 | self.storage["rain_duration_today"] = 0
229 | self.storage["lightning_count_today"] = 0
230 | self.last_midnight = self.cnv.utc_last_midnight()
231 | self.sql.writeStorage(self.storage)
232 | self.sql.dailyHousekeeping()
233 | self.current_day = datetime.today().weekday()
234 |
235 | if self.forecast is not None:
236 | await self._update_forecast()
237 |
238 | def _add_to_queue(
239 | self, topic: str, payload: str | None = None, qos: int = 0, retain: bool = False
240 | ) -> None:
241 | """Add an item to the queue."""
242 | self._queue.put_nowait((topic, payload, qos, retain))
243 |
244 | def _device_discovered(self, device: WeatherFlowDevice) -> None:
245 | """Handle a discovered device."""
246 |
247 | def _load_complete():
248 | _LOGGER.debug("Found device: %s", device)
249 | self._setup_sensors(device)
250 | device.on(
251 | EVENT_STATUS_UPDATE,
252 | lambda event: self._handle_status_update_event(device, event),
253 | )
254 | if isinstance(device, WeatherFlowSensorDevice):
255 | device.on(
256 | EVENT_OBSERVATION,
257 | lambda event: self._handle_observation_event(device, event),
258 | )
259 | if isinstance(device, AirSensorType):
260 | device.on(
261 | EVENT_STRIKE,
262 | lambda event: self._handle_strike_event(device, event),
263 | )
264 | if isinstance(device, SkySensorType):
265 | device.on(
266 | EVENT_RAPID_WIND,
267 | lambda event: self._handle_wind_event(device, event),
268 | )
269 | device.on(
270 | EVENT_RAIN_START,
271 | lambda event: self._handle_rain_start_event(device, event),
272 | )
273 |
274 | device.on(EVENT_LOAD_COMPLETE, lambda _: _load_complete())
275 |
276 | def _get_sensor_payload(
277 | self,
278 | sensor: BaseSensorDescription,
279 | device: WeatherFlowDevice,
280 | state_topic: str,
281 | attr_topic: str,
282 | ) -> OrderedDict:
283 | """Construct and return a sensor payload."""
284 | payload = OrderedDict()
285 | model = device.model
286 | serial_number = device.serial_number
287 |
288 | payload["name"] = f"{sensor.name}"
289 | payload["unique_id"] = f"{serial_number}-{sensor.id}"
290 | if (units := sensor.unit_i if self.is_imperial else sensor.unit_m) is not None:
291 | payload["unit_of_measurement"] = units
292 | if (device_class := sensor.device_class) is not None:
293 | payload["device_class"] = device_class
294 | if (state_class := sensor.state_class) is not None:
295 | payload["state_class"] = state_class
296 | if (icon := sensor.icon) is not None:
297 | payload["icon"] = f"mdi:{icon}"
298 | payload["state_topic"] = state_topic
299 | payload["value_template"] = f"{{{{ value_json.{sensor.id} }}}}"
300 | payload["json_attributes_topic"] = attr_topic
301 | payload["device"] = {
302 | "identifiers": [f"{DOMAIN}_{serial_number}"],
303 | "manufacturer": MANUFACTURER,
304 | "name": f"{model} {serial_number}",
305 | "model": model,
306 | "sw_version": device.firmware_revision,
307 | **(
308 | {"via_device": f"{DOMAIN}_{device.hub_sn}"}
309 | if isinstance(device, WeatherFlowSensorDevice)
310 | else {}
311 | ),
312 | }
313 |
314 | return payload
315 |
316 | def _handle_observation_event(
317 | self, device: WeatherFlowSensorDevice, event: CustomEvent
318 | ) -> None:
319 | """Handle an observation event."""
320 | _LOGGER.debug("Observation event from: %s", device)
321 |
322 | # Set some class level variables to help with sensors that may not have all data points available
323 | if (val := getattr(device, "solar_radiation", None)) is not None:
324 | self.solar_radiation = val.m
325 | if (
326 | val := getattr(device, "rain_accumulation_previous_minute", None)
327 | ) is not None:
328 | if val.m > 0:
329 | self.storage["rain_today"] += val.m
330 | self.storage["rain_duration_today"] += 1
331 | self.sql.writeStorage(self.storage)
332 |
333 | event_data: dict[str, OrderedDict] = {}
334 |
335 | for sensor in DEVICE_SENSORS:
336 | # Skip if this device is missing the attribute
337 | if (
338 | sensor.event in (EVENT_RAPID_WIND, EVENT_STATUS_UPDATE)
339 | or not hasattr(device, sensor.device_attr)
340 | or (
341 | sensor.id == "battery_mode"
342 | and not isinstance(device, TempestDevice)
343 | )
344 | ):
345 | continue
346 |
347 | attr = getattr(device, sensor.device_attr)
348 |
349 | if sensor.event not in event_data:
350 | event_data[sensor.event] = OrderedDict()
351 |
352 | try:
353 | if isinstance(sensor, SensorDescription):
354 | # TODO: Handle unique data points more elegantly...
355 | if sensor.id == "pressure_trend":
356 | continue
357 |
358 | if isinstance(attr, Callable):
359 | inputs = {}
360 | if "altitude" in sensor.inputs:
361 | inputs["altitude"] = self.elevation * UNIT_METERS
362 | attr = attr(**inputs)
363 |
364 | # Check for a custom function
365 | _data = event_data[EVENT_OBSERVATION]
366 |
367 | if (fn := sensor.custom_fn) is not None:
368 | # TODO: Handle unique data points more elegantly
369 | if sensor.id == "feelslike":
370 | attr = fn(self.cnv, device, self.wind_speed)
371 | elif sensor.id in ("visibility"):
372 | attr = fn(self.cnv, device, self.elevation)
373 | elif sensor.id == "wbgt":
374 | attr = fn(self.cnv, device, self.solar_radiation)
375 | elif sensor.id == "solar_elevation":
376 | self.solar_elevation = fn(self.cnv, self.latitude, self.longitude)
377 | attr = self.solar_elevation
378 | elif sensor.id == "solar_insolation":
379 | self.solar_insolation = fn(self.cnv, self.elevation, self.latitude, self.longitude)
380 | attr = self.solar_insolation
381 | elif sensor.id == "zambretti_number":
382 | self.zambretti_number = fn(self.cnv, self.latitude, _data.get("wind_bearing_avg"), self.sealevel_pressure_all_high, self.sealevel_pressure_all_low, self.pressure_trend, self.sealevel_pressure)
383 | attr = self.zambretti_number
384 | elif sensor.id == "zambretti_text":
385 | attr = fn(self.cnv, self.zambretti_number)
386 | elif sensor.id == "fog_probability":
387 | self.fog_probability = fn(self.cnv, self.solar_elevation, self.wind_speed, _data.get("relative_humidity"), _data.get("dewpoint"), _data.get("air_temperature"))
388 | attr = self.fog_probability
389 | elif sensor.id == "snow_probability":
390 | self.snow_probability = fn(self.cnv, device, _data.get("freezing_level"), _data.get("cloud_base"), self.elevation)
391 | attr = self.snow_probability
392 | elif sensor.id == "current_conditions":
393 | attr = fn(self.cnv, _data.get("lightning_strike_count_1hr"), _data.get("precipitation_type"), _data.get("rain_rate"), self.wind_speed, self.solar_elevation, self.solar_radiation, self.solar_insolation, self.snow_probability, self.fog_probability)
394 | else:
395 | attr = fn(self.cnv, device)
396 |
397 | # Check if a description is included
398 | if sensor.has_description and isinstance(attr, tuple):
399 | (
400 | attr,
401 | event_data[sensor.event][f"{sensor.id}_description"],
402 | ) = attr
403 |
404 | # Check if the attr is a Quantity object
405 | elif isinstance(attr, Quantity):
406 | # See if conversion is needed
407 | if (
408 | unit := sensor.imperial_unit
409 | if self.is_imperial
410 | else sensor.metric_unit
411 | ) is not None:
412 | attr = attr.to(unit)
413 |
414 | # Set the attribute to the Quantity's magnitude
415 | attr = attr.m
416 |
417 | # Check if rounding is needed
418 | if (
419 | attr is not None
420 | and (decimals := sensor.decimals[1 if self.is_imperial else 0])
421 | is not None
422 | ):
423 | attr = round(attr, decimals)
424 |
425 | elif isinstance(sensor, SqlSensorDescription):
426 | attr = sensor.sql_fn(self.sql)
427 |
428 | elif isinstance(sensor, StorageSensorDescription):
429 | attr = sensor.value(self.storage)
430 |
431 | if (fn := sensor.cnv_fn) is not None:
432 | attr = fn(self.cnv, attr)
433 |
434 | # Handle timestamp None value
435 | if sensor.device_class == DEVICE_CLASS_TIMESTAMP and attr is None:
436 | continue
437 |
438 | # Set the attribute in the payload
439 | event_data[sensor.event][sensor.id] = attr
440 | _LOGGER.debug("Setting payload: %s = %s", sensor.id, attr)
441 | except Exception as ex:
442 | _LOGGER.error("Error setting sensor data for %s: %s", sensor.id, ex)
443 |
444 | data = event_data[EVENT_OBSERVATION]
445 |
446 | # TODO: Handle unique data points more elegantly...
447 | if data.get("sealevel_pressure") is not None:
448 | (
449 | data["pressure_trend"],
450 | data["pressure_trend_value"],
451 | ) = self.sql.readPressureTrend(
452 | data["sealevel_pressure"], self.cnv.translations
453 | )
454 | self.pressure_trend = data["pressure_trend_value"]
455 | self.sealevel_pressure = data["sealevel_pressure"]
456 | self.sql.writePressure(data["sealevel_pressure"])
457 |
458 | data["last_reset_midnight"] = self.last_midnight
459 |
460 | for (evt, data) in event_data.items():
461 | if data:
462 | state_topic = MQTT_TOPIC_FORMAT.format(
463 | DEVICE_SERIAL_FORMAT.format(device.serial_number), evt, "state"
464 | )
465 | self._add_to_queue(state_topic, json.dumps(data))
466 |
467 | self.sql.updateHighLow(event_data[EVENT_OBSERVATION])
468 | # self.sql.updateDayData(event_data[EVENT_OBSERVATION])
469 |
470 | self._send_high_low_update(device=device)
471 |
472 | def _handle_rain_start_event(
473 | self, device: SkySensorType, event: RainStartEvent
474 | ) -> None:
475 | """Handle a rain start event."""
476 | _LOGGER.debug("Rain start event from: %s", device)
477 | self.storage["rain_start"] = event.epoch
478 | self.sql.writeStorage(self.storage)
479 |
480 | def _handle_status_update_event(
481 | self, device: HubDevice | WeatherFlowSensorDevice, event: CustomEvent
482 | ) -> None:
483 | """Handle a hub status event."""
484 | _LOGGER.debug("Status update event from: %s", device)
485 | device_serial = DEVICE_SERIAL_FORMAT.format(device.serial_number)
486 |
487 | state_topic = MQTT_TOPIC_FORMAT.format(
488 | device_serial, EVENT_STATUS_UPDATE, "state"
489 | )
490 | state_data = OrderedDict()
491 | state_data["status"] = device.up_since.isoformat()
492 | self._add_to_queue(state_topic, json.dumps(state_data))
493 |
494 | attr_topic = MQTT_TOPIC_FORMAT.format(device_serial, "status", "attributes")
495 | attr_data = OrderedDict()
496 | attr_data[ATTR_ATTRIBUTION] = ATTRIBUTION
497 | attr_data["serial_number"] = device.serial_number
498 | attr_data["rssi"] = device.rssi.m
499 | attr_data["version"] = VERSION
500 |
501 | if isinstance(device, HubDevice):
502 | attr_data["reset_flags"] = device.reset_flags
503 | _LOGGER.debug("HUB Reset Flags: %s", device.reset_flags)
504 | else:
505 | attr_data["voltage"] = device._voltage
506 | attr_data["sensor_status"] = device.sensor_status
507 |
508 | if device.sensor_status:
509 | _LOGGER.debug(
510 | "Device %s has reported a sensor fault. Reason: %s",
511 | device.serial_number,
512 | device.sensor_status,
513 | )
514 | _LOGGER.debug(
515 | "DEVICE STATUS TRIGGERED AT %s\n -- Device: %s\n -- Firmware Revision: %s\n -- Voltage: %s",
516 | str(datetime.now()),
517 | device.serial_number,
518 | device.firmware_revision,
519 | device._voltage,
520 | )
521 |
522 | self._add_to_queue(attr_topic, json.dumps(attr_data))
523 |
524 | def _handle_strike_event(
525 | self, device: AirSensorType, event: LightningStrikeEvent
526 | ) -> None:
527 | """Handle a strike event."""
528 | _LOGGER.debug("Lightning strike event from: %s", device)
529 | self.sql.writeLightning()
530 | self.storage["lightning_count_today"] += 1
531 | self.storage["last_lightning_distance"] = self.cnv.distance(event.distance.m)
532 | self.storage["last_lightning_energy"] = event.energy
533 | self.storage["last_lightning_time"] = event.epoch
534 | self.sql.writeStorage(self.storage)
535 |
536 | def _handle_wind_event(self, device: SkySensorType, event: WindEvent) -> None:
537 | """Handle a wind event."""
538 | _LOGGER.debug("Wind event from: %s", device)
539 | data = OrderedDict()
540 | state_topic = MQTT_TOPIC_FORMAT.format(
541 | DEVICE_SERIAL_FORMAT.format(device.serial_number), EVENT_RAPID_WIND, "state"
542 | )
543 | now = datetime.now().timestamp()
544 | if (now - self.rapid_last_run) >= self.rapid_wind_interval:
545 | data["wind_speed"] = self.cnv.speed(event.speed.m)
546 | data["wind_bearing"] = event.direction.m
547 | data["wind_direction"] = self.cnv.direction(event.direction.m)
548 | self.wind_speed = event.speed.m
549 | self._add_to_queue(state_topic, json.dumps(data))
550 | self.rapid_last_run = datetime.now().timestamp()
551 |
552 | def _init_sql_db(self, database_file: str = None) -> None:
553 | """Initialize the self.sqlite DB."""
554 | self.sql = SQLFunctions(self.unit_system)
555 | database_exist = os.path.isfile(database_file)
556 | self.sql.create_connection(database_file)
557 | if not database_exist:
558 | self.sql.createInitialDataset()
559 | # Upgrade Database if needed
560 | self.sql.upgradeDatabase()
561 |
562 | self.storage = self.sql.readStorage()
563 |
564 | def _send_high_low_update(self, device: WeatherFlowSensorDevice) -> None:
565 | # Update High and Low values if it is time
566 | now = datetime.now().timestamp()
567 | if (now - self.high_low_last_run) >= HIGH_LOW_TIMER:
568 | highlow_topic = MQTT_TOPIC_FORMAT.format(
569 | DEVICE_SERIAL_FORMAT.format(device.serial_number),
570 | EVENT_HIGH_LOW,
571 | "attributes",
572 | )
573 | high_low_data = self.sql.readHighLow()
574 | self._add_to_queue(
575 | highlow_topic, json.dumps(high_low_data), qos=1, retain=True
576 | )
577 | self.high_low_last_run = datetime.now().timestamp()
578 |
579 | def _setup_mqtt_client(self) -> MqttClient:
580 | """Initialize MQTT client."""
581 | if (
582 | anonymous := not self.mqtt_config.username or not self.mqtt_config.password
583 | ) and self.mqtt_config.debug:
584 | _LOGGER.debug("MQTT Credentials not needed")
585 |
586 | self.mqtt_client = client = MqttClient()
587 |
588 | if not anonymous:
589 | client.username_pw_set(
590 | username=self.mqtt_config.username, password=self.mqtt_config.password
591 | )
592 | if self.mqtt_config.debug:
593 | client.enable_logger()
594 | _LOGGER.debug(
595 | "MQTT Credentials: %s - %s",
596 | self.mqtt_config.username,
597 | self.mqtt_config.password,
598 | )
599 |
600 | def _setup_sensors(self, device: WeatherFlowDevice) -> None:
601 | """Create Sensors in Home Assistant."""
602 | serial_number = device.serial_number
603 | domain_serial = DEVICE_SERIAL_FORMAT.format(serial_number)
604 |
605 | SENSORS = (
606 | DEVICE_SENSORS
607 | if isinstance(device, WeatherFlowSensorDevice)
608 | else HUB_SENSORS
609 | )
610 |
611 | # Create the config for the Sensors
612 | for sensor in SENSORS:
613 | sensor_id = sensor.id
614 | sensor_event = sensor.event
615 |
616 | if not hasattr(device, sensor.device_attr) or (
617 | sensor.id == "battery_mode" and not isinstance(device, TempestDevice)
618 | ):
619 | # Don't add sensors for devices that don't report on that attribute
620 | continue
621 |
622 | state_topic = MQTT_TOPIC_FORMAT.format(domain_serial, sensor_event, "state")
623 | attr_topic = MQTT_TOPIC_FORMAT.format(
624 | domain_serial, sensor_id, "attributes"
625 | )
626 | discovery_topic = MQTT_TOPIC_FORMAT.format(
627 | domain_serial, sensor_id, "config"
628 | )
629 |
630 | attribution = OrderedDict()
631 | payload: OrderedDict | None = None
632 |
633 | if self._filter_sensors is None or (
634 | (sensor_id in self._filter_sensors) is not self._invert_filter
635 | ):
636 | _LOGGER.info("Setting up %s sensor: %s", device.model, sensor.name)
637 |
638 | # Payload
639 | payload = self._get_sensor_payload(
640 | sensor=sensor,
641 | device=device,
642 | state_topic=state_topic,
643 | attr_topic=attr_topic,
644 | )
645 |
646 | # Attributes
647 | attribution[ATTR_ATTRIBUTION] = ATTRIBUTION
648 |
649 | # Add description if needed
650 | if sensor.has_description:
651 | payload["json_attributes_topic"] = state_topic
652 | template = OrderedDict()
653 | template = attribution
654 | template[
655 | "description"
656 | ] = f"{{{{ value_json.{sensor_id}_description }}}}"
657 | payload["json_attributes_template"] = json.dumps(template)
658 |
659 | # Add additional attributes to some sensors
660 | if sensor_id == "pressure_trend":
661 | payload["json_attributes_topic"] = state_topic
662 | template = OrderedDict()
663 | template = attribution
664 | template["trend_value"] = "{{ value_json.pressure_trend_value }}"
665 | payload["json_attributes_template"] = json.dumps(template)
666 |
667 | # Add extra attributes if needed
668 | if sensor.extra_att:
669 | payload["json_attributes_topic"] = MQTT_TOPIC_FORMAT.format(
670 | domain_serial, EVENT_HIGH_LOW, "attributes"
671 | )
672 | template = OrderedDict()
673 | template = attribution
674 | template["max_day"] = f"{{{{ value_json.{sensor_id}['max_day'] }}}}"
675 | template[
676 | "max_day_time"
677 | ] = f"{{{{ value_json.{sensor_id}['max_day_time'] }}}}"
678 | template[
679 | "max_month"
680 | ] = f"{{{{ value_json.{sensor_id}['max_month'] }}}}"
681 | template[
682 | "max_month_time"
683 | ] = f"{{{{ value_json.{sensor_id}['max_month_time'] }}}}"
684 | template["max_all"] = f"{{{{ value_json.{sensor_id}['max_all'] }}}}"
685 | template[
686 | "max_all_time"
687 | ] = f"{{{{ value_json.{sensor_id}['max_all_time'] }}}}"
688 | if sensor.show_min_att:
689 | template[
690 | "min_day"
691 | ] = f"{{{{ value_json.{sensor_id}['min_day'] }}}}"
692 | template[
693 | "min_day_time"
694 | ] = f"{{{{ value_json.{sensor_id}['min_day_time'] }}}}"
695 | template[
696 | "min_month"
697 | ] = f"{{{{ value_json.{sensor_id}['min_month'] }}}}"
698 | template[
699 | "min_month_time"
700 | ] = f"{{{{ value_json.{sensor_id}['min_month_time'] }}}}"
701 | template[
702 | "min_all"
703 | ] = f"{{{{ value_json.{sensor_id}['min_all'] }}}}"
704 | template[
705 | "min_all_time"
706 | ] = f"{{{{ value_json.{sensor_id}['min_all_time'] }}}}"
707 | payload["json_attributes_template"] = json.dumps(template)
708 |
709 | self._add_to_queue(
710 | discovery_topic, json.dumps(payload or {}), qos=1, retain=True
711 | )
712 | self._add_to_queue(attr_topic, json.dumps(attribution), qos=1, retain=True)
713 |
714 | if isinstance(device, HubDevice):
715 | run_forecast = False
716 | fcst_state_topic = MQTT_TOPIC_FORMAT.format(
717 | DOMAIN, FORECAST_ENTITY, "state"
718 | )
719 | fcst_attr_topic = MQTT_TOPIC_FORMAT.format(
720 | DOMAIN, FORECAST_ENTITY, "attributes"
721 | )
722 | for sensor in FORECAST_SENSORS:
723 | discovery_topic = MQTT_TOPIC_FORMAT.format(DOMAIN, sensor.id, "config")
724 | payload: OrderedDict | None = None
725 | if self.forecast is not None:
726 | _LOGGER.info("Setting up %s sensor: %s", device.model, sensor.name)
727 | run_forecast = True
728 | payload = self._get_sensor_payload(
729 | sensor=sensor,
730 | device=device,
731 | state_topic=fcst_state_topic,
732 | attr_topic=fcst_attr_topic,
733 | )
734 | self._add_to_queue(
735 | discovery_topic, json.dumps(payload or {}), qos=1, retain=True
736 | )
737 |
738 | if run_forecast:
739 | asyncio.ensure_future(self._update_forecast())
740 |
741 | # cleanup obsolete sensors
742 | for sensor in OBSOLETE_SENSORS:
743 | self._add_to_queue(
744 | topic=MQTT_TOPIC_FORMAT.format(domain_serial, sensor, "config")
745 | )
746 |
747 | async def _mqtt_queue_processor(self) -> None:
748 | """MQTT queue processor."""
749 | while True:
750 | topic, payload, qos, retain = await self._queue.get()
751 | await self._publish_mqtt(topic, payload, qos, retain)
752 | self._queue.task_done()
753 |
754 | async def _publish_mqtt(
755 | self, topic: str, payload: str | None = None, qos: int = 0, retain: bool = False
756 | ) -> None:
757 | """Publish a MQTT topic with payload."""
758 | try:
759 | self.mqtt_client.publish(topic, payload, qos=qos, retain=retain)
760 | except Exception as e:
761 | _LOGGER.error("Could not connect to MQTT Server. Error is: %s", e)
762 | await asyncio.sleep(0.01)
763 |
764 | async def _update_forecast(self) -> None:
765 | """Attempt to update the forecast."""
766 | # Update the Forecast if it is time and enabled
767 | assert self.forecast
768 | if (now := datetime.now().timestamp()) >= self._forecast_next_run:
769 | if any(forecast := await self.forecast.update_forecast()):
770 | _LOGGER.debug("Sending updated forecast data to MQTT")
771 | for topic, data in zip(("state", "attributes"), forecast):
772 | self._add_to_queue(
773 | MQTT_TOPIC_FORMAT.format(DOMAIN, FORECAST_ENTITY, topic),
774 | json.dumps(data),
775 | qos=1,
776 | retain=True,
777 | )
778 | self._forecast_next_run = now + self.forecast.interval * 60
779 | else:
780 | _LOGGER.debug(
781 | "Forecast update will run in ~%s minutes",
782 | ceil((self._forecast_next_run - now) / 60),
783 | )
784 |
785 |
786 | async def main():
787 | """Entry point for program."""
788 | logging.basicConfig(level=logging.INFO)
789 |
790 | try:
791 | if is_supervisor := truebool(os.getenv("HA_SUPERVISOR")):
792 | config = await get_supervisor_configuration()
793 | else:
794 | config = os.environ
795 | except:
796 | config = os.environ
797 |
798 | _LOGGER.info("Timezone is %s", os.environ.get("TZ"))
799 |
800 | # Read the config Settings
801 | elevation = float(config.get("ELEVATION", 0))
802 | latitude = float(config.get("LATITUDE", 0))
803 | longitude = float(config.get("LONGITUDE", 0))
804 | unit_system = config.get("UNIT_SYSTEM", UNITS_METRIC)
805 | _LOGGER.info("Unit System is %s", unit_system)
806 | rw_interval = int(config.get("RAPID_WIND_INTERVAL", 0))
807 | language = config.get("LANGUAGE", LANGUAGE_ENGLISH).lower()
808 | zambretti_min_default = ZAMBRETTI_MIN_PRESSURE if unit_system == UNITS_METRIC else ZAMBRETTI_MIN_PRESSURE * 0.029530
809 | zambretti_max_default = ZAMBRETTI_MAX_PRESSURE if unit_system == UNITS_METRIC else ZAMBRETTI_MAX_PRESSURE * 0.029530
810 | zambretti_min_pressure = float(config.get("ZAMBRETTI_MIN_PRESSURE", zambretti_min_default))
811 | zambretti_max_pressure = float(config.get("ZAMBRETTI_MAX_PRESSURE", zambretti_max_default))
812 | _LOGGER.info("Zambretti Values: %s and %s", zambretti_min_pressure, zambretti_max_pressure)
813 |
814 | mqtt_config = MqttConfig(
815 | host=config.get("MQTT_HOST", "127.0.0.1"),
816 | port=int(config.get("MQTT_PORT", 1883)),
817 | username=config.get("MQTT_USERNAME"),
818 | password=config.get("MQTT_PASSWORD"),
819 | debug=truebool(config.get("MQTT_DEBUG")),
820 | )
821 |
822 | udp_config = WeatherFlowUdpConfig(
823 | host=config.get("WF_HOST", "0.0.0.0"), port=int(config.get("WF_PORT", 50222))
824 | )
825 |
826 | forecast_config = (
827 | ForecastConfig(
828 | station_id=station_id,
829 | token=station_token,
830 | interval=int(config.get("FORECAST_INTERVAL", 30)),
831 | )
832 | if (
833 | (station_id := config.get("STATION_ID"))
834 | and (station_token := config.get("STATION_TOKEN"))
835 | )
836 | else None
837 | )
838 |
839 | if truebool(config.get("DEBUG")):
840 | logging.getLogger().setLevel(logging.DEBUG)
841 |
842 | if isinstance(filter_sensors := config.get("FILTER_SENSORS"), str):
843 | filter_sensors = [sensor.strip() for sensor in filter_sensors.split(",")]
844 | invert_filter = truebool(config.get("INVERT_FILTER"))
845 |
846 | # Read the sensor config
847 | if filter_sensors is None and not is_supervisor:
848 | filter_sensors = read_config()
849 |
850 | weatherflowmqtt = WeatherFlowMqtt(
851 | elevation=elevation,
852 | latitude=latitude,
853 | longitude=longitude,
854 | unit_system=unit_system,
855 | rapid_wind_interval=rw_interval,
856 | language=language,
857 | mqtt_config=mqtt_config,
858 | udp_config=udp_config,
859 | forecast_config=forecast_config,
860 | database_file=DATABASE,
861 | filter_sensors=filter_sensors,
862 | invert_filter=invert_filter,
863 | zambretti_min_pressure=zambretti_min_pressure,
864 | zambretti_max_pressure=zambretti_max_pressure,
865 | )
866 | await weatherflowmqtt.connect()
867 |
868 | # Watch for message from the UDP socket
869 | while weatherflowmqtt.listener.is_listening:
870 | await asyncio.sleep(60)
871 | await weatherflowmqtt.run_time_based_updates()
872 |
873 |
874 | async def get_supervisor_configuration() -> dict[str, Any]:
875 | """Get the configuration from Home Assistant Supervisor."""
876 | from aiohttp import ClientSession
877 |
878 | _LOGGER.info("🏠 Home Assistant Supervisor Mode 🏠")
879 |
880 | config: dict[str, Any] = {}
881 |
882 | supervisor_url = "http://supervisor"
883 | headers = {"Authorization": "Bearer " + os.getenv("SUPERVISOR_TOKEN")}
884 |
885 | async with ClientSession() as session:
886 | try:
887 | async with session.get(
888 | supervisor_url + "/core/api/config",
889 | headers=headers,
890 | ) as resp:
891 | if (data := await resp.json()) is not None:
892 | config.update(
893 | {
894 | "ELEVATION": data.get("elevation"),
895 | "LATITUDE": data.get("latitude"),
896 | "LONGITUDE": data.get("longitude"),
897 | "UNIT_SYSTEM": UNITS_METRIC
898 | if data.get("unit_system", {}).get("temperature")
899 | == TEMP_CELSIUS
900 | else UNITS_IMPERIAL,
901 | }
902 | )
903 | _LOGGER.info("Add-On value Unit System is: %s", data.get("unit_system", {}))
904 | except Exception as e:
905 | _LOGGER.error("Could not read Home Assistant core config: %s", e)
906 |
907 | try:
908 | async with session.get(
909 | supervisor_url + "/services/mqtt",
910 | headers=headers,
911 | ) as resp:
912 | resp_json = await resp.json()
913 | if "ok" in resp_json.get("result"):
914 | data = resp_json["data"]
915 | config.update(
916 | {
917 | "MQTT_HOST": data["host"],
918 | "MQTT_PORT": data["port"],
919 | "MQTT_USERNAME": data["username"],
920 | "MQTT_PASSWORD": data["password"],
921 | }
922 | )
923 | except Exception as e:
924 | _LOGGER.error("Could not read Home Assistant MQTT config: %s", e)
925 |
926 | if os.path.exists(options_file := f"{EXTERNAL_DIRECTORY}/options.json"):
927 | with open(options_file, "r") as f:
928 | config.update(json.load(f))
929 |
930 | return config
931 |
932 |
933 | # Main Program starts
934 | if __name__ == "__main__":
935 | try:
936 | asyncio.run(main())
937 | except KeyboardInterrupt:
938 | print("\nExiting Program")
939 |
--------------------------------------------------------------------------------