├── .github ├── workflows │ ├── constraints.txt │ ├── release-drafter.yml │ ├── labeler.yml │ ├── auto-merge.yml │ └── tests.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── issue.md ├── release-drafter.yml └── labels.yml ├── requirements.txt ├── md.images └── overview.png ├── scripts ├── lint ├── setup └── develop ├── hacs.json ├── config └── configuration.yaml ├── .gitignore ├── .vscode └── tasks.json ├── custom_components └── integration_ecowitt │ ├── manifest.json │ ├── schemas.py │ ├── translations │ └── en.json │ ├── binary_sensor.py │ ├── sensor.py │ ├── config_flow.py │ ├── __init__.py │ └── const.py ├── .pre-commit-config.yaml ├── .devcontainer.json ├── setup.cfg ├── .ruff.toml ├── tests └── fake_client.py ├── README.md └── LICENSE /.github/workflows/constraints.txt: -------------------------------------------------------------------------------- 1 | pre-commit==4.0.1 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorlog 2 | homeassistant==2025.1.0 3 | pyecowitt==0.14 4 | -------------------------------------------------------------------------------- /md.images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PoppyPop/homeassistant_ecowitt/HEAD/md.images/overview.png -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | ruff check . --fix 8 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ecowitt Weather Station", 3 | "render_readme": true, 4 | "hacs": "1.6.0", 5 | "homeassistant": "2023.5.0" 6 | } 7 | -------------------------------------------------------------------------------- /config/configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | 3 | logger: 4 | default: info 5 | logs: 6 | custom_components.ecowitt: debug 7 | # If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) 8 | # debugpy: 9 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | python3 -m pip install --requirement requirements.txt 8 | 9 | python3 -m pip install --requirement .github/workflows/constraints.txt 10 | 11 | pre-commit install 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # artifacts 2 | __pycache__ 3 | .pytest* 4 | *.egg-info 5 | */build/* 6 | */dist/* 7 | 8 | 9 | # misc 10 | .coverage 11 | .vscode 12 | coverage.xml 13 | 14 | 15 | # Home Assistant configuration 16 | config/* 17 | !config/configuration.yaml 18 | 19 | .ruff_cache 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: pip 8 | directory: "/.github/workflows" 9 | schedule: 10 | interval: weekly 11 | - package-ecosystem: pip 12 | directory: "/" 13 | schedule: 14 | interval: weekly 15 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Draft a release note 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | jobs: 8 | draft_release: 9 | name: Release Drafter 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Run release-drafter 13 | uses: release-drafter/release-drafter@v5.19.0 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run Home Assistant on port 8123", 6 | "type": "shell", 7 | "command": "scripts/develop", 8 | "problemMatcher": [] 9 | }, 10 | { 11 | "label": "Launch fake ecowitt client", 12 | "type": "shell", 13 | "command": "python tests/fake_client.py", 14 | "problemMatcher": [] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Manage labels 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | jobs: 10 | labeler: 11 | name: Labeler 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out the repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Run Labeler 18 | uses: crazy-max/ghaction-github-labeler@v3.1.1 19 | with: 20 | skip-delete: true 21 | -------------------------------------------------------------------------------- /custom_components/integration_ecowitt/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "integration_ecowitt", 3 | "name": "Ecowitt Weather Station", 4 | "codeowners": ["@garbled1", "@PoppyPop"], 5 | "config_flow": true, 6 | "documentation": "https://github.com/PoppyPop/homeassistant_ecowitt", 7 | "iot_class": "local_push", 8 | "issue_tracker": "https://github.com/PoppyPop/homeassistant_ecowitt/issues", 9 | "requirements": ["pyecowitt==0.14"], 10 | "version": "0.2.1" 11 | } 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - repo: https://github.com/astral-sh/ruff-pre-commit 10 | # Ruff version. 11 | rev: v0.6.4 12 | hooks: 13 | # Run the linter. 14 | - id: ruff 15 | args: [--fix] 16 | # Run the formatter. 17 | - id: ruff-format 18 | - repo: https://github.com/pre-commit/mirrors-prettier 19 | rev: v2.2.1 20 | hooks: 21 | - id: prettier 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /scripts/develop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | # Create config dir if not present 8 | if [[ ! -d "${PWD}/config" ]]; then 9 | mkdir -p "${PWD}/config" 10 | hass --config "${PWD}/config" --script ensure_config 11 | fi 12 | 13 | # Set the path to custom_components 14 | ## This let's us have the structure we want /custom_components/integration_blueprint 15 | ## while at the same time have Home Assistant configuration inside /config 16 | ## without resulting to symlinks. 17 | export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" 18 | 19 | # Start Home Assistant 20 | hass --config "${PWD}/config" --debug 21 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: ":boom: Breaking Changes" 3 | label: "breaking" 4 | - title: ":rocket: Features" 5 | label: "enhancement" 6 | - title: ":fire: Removals and Deprecations" 7 | label: "removal" 8 | - title: ":beetle: Fixes" 9 | label: "bug" 10 | - title: ":racehorse: Performance" 11 | label: "performance" 12 | - title: ":rotating_light: Testing" 13 | label: "testing" 14 | - title: ":construction_worker: Continuous Integration" 15 | label: "ci" 16 | - title: ":books: Documentation" 17 | label: "documentation" 18 | - title: ":hammer: Refactoring" 19 | label: "refactoring" 20 | - title: ":lipstick: Style" 21 | label: "style" 22 | - title: ":package: Dependencies" 23 | labels: 24 | - "dependencies" 25 | - "build" 26 | template: | 27 | ## Changes 28 | 29 | $CHANGES 30 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1.4.0 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'}} 20 | run: gh pr merge --auto --merge "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Create a report to help us improve 4 | --- 5 | 6 | 15 | 16 | ## Version of the custom_component 17 | 18 | 21 | 22 | ## Configuration 23 | 24 | ```yaml 25 | Add your logs here. 26 | ``` 27 | 28 | ## Describe the bug 29 | 30 | A clear and concise description of what the bug is. 31 | 32 | ## Debug log 33 | 34 | 35 | 36 | ```text 37 | 38 | Add your logs here. 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /custom_components/integration_ecowitt/schemas.py: -------------------------------------------------------------------------------- 1 | """Schema definitions.""" 2 | 3 | import homeassistant.helpers.config_validation as cv 4 | import voluptuous as vol 5 | from homeassistant.const import CONF_PORT 6 | 7 | from .const import CONF_NAME 8 | from .const import CONF_UNIT_BARO 9 | from .const import CONF_UNIT_LIGHTNING 10 | from .const import CONF_UNIT_RAIN 11 | from .const import CONF_UNIT_WIND 12 | from .const import CONF_UNIT_WINDCHILL 13 | from .const import DEFAULT_PORT 14 | from .const import DOMAIN 15 | from .const import W_TYPE_HYBRID 16 | 17 | COMPONENT_SCHEMA = vol.Schema( 18 | { 19 | vol.Required(CONF_PORT): cv.port, 20 | vol.Optional(CONF_UNIT_BARO): cv.string, 21 | vol.Optional(CONF_UNIT_WIND): cv.string, 22 | vol.Optional(CONF_UNIT_RAIN): cv.string, 23 | vol.Optional(CONF_UNIT_LIGHTNING): cv.string, 24 | vol.Optional(CONF_UNIT_WINDCHILL, default=W_TYPE_HYBRID): cv.string, 25 | } 26 | ) 27 | 28 | CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA) 29 | 30 | DATA_SCHEMA = vol.Schema( 31 | { 32 | vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, 33 | vol.Optional(CONF_NAME, description={"suggested_value": "ecowitt"}): str, 34 | } 35 | ) 36 | -------------------------------------------------------------------------------- /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homeassistat_ecowitt", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.12", 4 | "postCreateCommand": "scripts/setup", 5 | "forwardPorts": [8123], 6 | "portsAttributes": { 7 | "8123": { 8 | "label": "Home Assistant", 9 | "onAutoForward": "notify" 10 | } 11 | }, 12 | "customizations": { 13 | "vscode": { 14 | "extensions": [ 15 | "ms-python.python", 16 | "github.vscode-pull-request-github", 17 | "ryanluker.vscode-coverage-gutters", 18 | "ms-python.vscode-pylance" 19 | ], 20 | "settings": { 21 | "files.eol": "\n", 22 | "editor.tabSize": 4, 23 | "python.pythonPath": "/usr/bin/python3", 24 | "python.analysis.autoSearchPaths": false, 25 | "python.linting.pylintEnabled": true, 26 | "python.linting.enabled": true, 27 | "python.formatting.provider": "black", 28 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 29 | "editor.formatOnPaste": false, 30 | "editor.formatOnSave": true, 31 | "editor.formatOnType": true, 32 | "files.trimTrailingWhitespace": true 33 | } 34 | } 35 | }, 36 | "remoteUser": "vscode", 37 | "features": { 38 | "ghcr.io/devcontainers/features/rust:1": {} 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build 3 | doctests = True 4 | # To work with Black 5 | max-line-length = 88 6 | # E501: line too long 7 | # W503: Line break occurred before a binary operator 8 | # E203: Whitespace before ':' 9 | # D202 No blank lines allowed after function docstring 10 | # W504 line break after binary operator 11 | ignore = 12 | E501, 13 | W503, 14 | E203, 15 | D202, 16 | W504 17 | 18 | [isort] 19 | # https://github.com/timothycrosley/isort 20 | # https://github.com/timothycrosley/isort/wiki/isort-Settings 21 | # splits long import on multiple lines indented by 4 spaces 22 | multi_line_output = 3 23 | include_trailing_comma=True 24 | force_grid_wrap=0 25 | use_parentheses=True 26 | line_length=88 27 | indent = " " 28 | # by default isort don't check module indexes 29 | not_skip = __init__.py 30 | # will group `import x` and `from x import` of the same module. 31 | force_sort_within_sections = true 32 | sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 33 | default_section = THIRDPARTY 34 | known_first_party = custom_components.teleinformation, tests 35 | combine_as_imports = true 36 | 37 | [tool:pytest] 38 | addopts = -qq --cov=custom_components.teleinformation 39 | console_output_style = count 40 | 41 | [coverage:run] 42 | branch = False 43 | 44 | [coverage:report] 45 | show_missing = true 46 | fail_under = 100 47 | -------------------------------------------------------------------------------- /custom_components/integration_ecowitt/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured.", 5 | "unknown": "Unknown error." 6 | }, 7 | "step": { 8 | "user": { 9 | "description": "The following steps must be performed before setting up this integration.\nIf you have not already done so, please do this now.\n\nUse the WS View app (on your phone) for your Ecowitt device, and connect to it.\nPick menu -> device list -> Pick your station.\nHit next repeatedly to move to the last screen titled 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath doesn't matter as long as it ends in /, leave the default, or change it to just /.\nPick a port that is not in use on the server (netstat -lt). (4199 is probably a good default)\nPick a reasonable value for updates, like 60 seconds.\nSave configuration. The Ecowitt should then start attempting to send data to your server.\n\nClick submit when these instructions have been completed.", 10 | "title": "Instructions for setting up the Ecowitt." 11 | }, 12 | "initial_options": { 13 | "title": "Ecowitt Parameters", 14 | "data": { 15 | "port": "Listening port" 16 | } 17 | } 18 | } 19 | }, 20 | "options": { 21 | "step": { 22 | "init": { 23 | "data": { 24 | "barounit": "Barometer Unit", 25 | "windunit": "Wind Unit", 26 | "rainunit": "Rainfall Unit", 27 | "lightningunit": "Lightning distance unit", 28 | "windchillunit": "Windchill calculation" 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | - dev 9 | pull_request: 10 | schedule: 11 | - cron: "0 0 * * *" 12 | 13 | env: 14 | DEFAULT_PYTHON: 3.9 15 | 16 | jobs: 17 | pre-commit: 18 | runs-on: "ubuntu-latest" 19 | name: Pre-commit 20 | steps: 21 | - name: Check out the repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 25 | uses: actions/setup-python@v3.1.1 26 | with: 27 | python-version: ${{ env.DEFAULT_PYTHON }} 28 | 29 | - name: Upgrade pip 30 | run: | 31 | pip install --constraint=.github/workflows/constraints.txt pip 32 | pip --version 33 | 34 | - name: Install Python modules 35 | run: | 36 | pip install --constraint=.github/workflows/constraints.txt pre-commit black flake8 reorder-python-imports 37 | 38 | - name: Run pre-commit on all files 39 | run: | 40 | pre-commit run --all-files --show-diff-on-failure --color=always 41 | 42 | hacs: 43 | runs-on: "ubuntu-latest" 44 | name: HACS 45 | steps: 46 | - name: Check out the repository 47 | uses: "actions/checkout@v3" 48 | 49 | - name: HACS validation 50 | uses: "hacs/action@22.5.0" 51 | with: 52 | category: "integration" 53 | ignore: brands 54 | 55 | hassfest: 56 | runs-on: "ubuntu-latest" 57 | name: Hassfest 58 | steps: 59 | - name: Check out the repository 60 | uses: "actions/checkout@v3" 61 | 62 | - name: Hassfest validation 63 | uses: "home-assistant/actions/hassfest@master" 64 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Labels names are important as they are used by Release Drafter to decide 3 | # regarding where to record them in changelog or if to skip them. 4 | # 5 | # The repository labels will be automatically configured using this file and 6 | # the GitHub Action https://github.com/marketplace/actions/github-labeler. 7 | - name: breaking 8 | description: Breaking Changes 9 | color: bfd4f2 10 | - name: bug 11 | description: Something isn't working 12 | color: d73a4a 13 | - name: build 14 | description: Build System and Dependencies 15 | color: bfdadc 16 | - name: ci 17 | description: Continuous Integration 18 | color: 4a97d6 19 | - name: dependencies 20 | description: Pull requests that update a dependency file 21 | color: 0366d6 22 | - name: documentation 23 | description: Improvements or additions to documentation 24 | color: 0075ca 25 | - name: duplicate 26 | description: This issue or pull request already exists 27 | color: cfd3d7 28 | - name: enhancement 29 | description: New feature or request 30 | color: a2eeef 31 | - name: github_actions 32 | description: Pull requests that update Github_actions code 33 | color: "000000" 34 | - name: good first issue 35 | description: Good for newcomers 36 | color: 7057ff 37 | - name: help wanted 38 | description: Extra attention is needed 39 | color: 008672 40 | - name: invalid 41 | description: This doesn't seem right 42 | color: e4e669 43 | - name: performance 44 | description: Performance 45 | color: "016175" 46 | - name: python 47 | description: Pull requests that update Python code 48 | color: 2b67c6 49 | - name: question 50 | description: Further information is requested 51 | color: d876e3 52 | - name: refactoring 53 | description: Refactoring 54 | color: ef67c4 55 | - name: removal 56 | description: Removals and Deprecations 57 | color: 9ae7ea 58 | - name: style 59 | description: Style 60 | color: c120e5 61 | - name: testing 62 | description: Testing 63 | color: b1fc6f 64 | - name: wontfix 65 | description: This will not be worked on 66 | color: ffffff 67 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | # The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml 2 | 3 | target-version = "py310" 4 | 5 | lint.select = [ 6 | "B007", # Loop control variable {name} not used within loop body 7 | "B014", # Exception handler with duplicate exception 8 | "C", # complexity 9 | "D", # docstrings 10 | "E", # pycodestyle 11 | "F", # pyflakes/autoflake 12 | "ICN001", # import concentions; {name} should be imported as {asname} 13 | "PGH004", # Use specific rule codes when using noqa 14 | "PLC0414", # Useless import alias. Import alias does not rename original package. 15 | "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass 16 | "SIM117", # Merge with-statements that use the same scope 17 | "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() 18 | "SIM201", # Use {left} != {right} instead of not {left} == {right} 19 | "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} 20 | "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. 21 | "SIM401", # Use get from dict with default instead of an if block 22 | "T20", # flake8-print 23 | "TRY004", # Prefer TypeError exception for invalid type 24 | "RUF006", # Store a reference to the return value of asyncio.create_task 25 | "UP", # pyupgrade 26 | "W", # pycodestyle 27 | ] 28 | 29 | lint.ignore = [ 30 | "D202", # No blank lines allowed after function docstring 31 | "D203", # 1 blank line required before class docstring 32 | "D213", # Multi-line docstring summary should start at the second line 33 | "D404", # First word of the docstring should not be This 34 | "D406", # Section name should end with a newline 35 | "D407", # Section name underlining 36 | "D411", # Missing blank line before section 37 | "E501", # line too long 38 | "E731", # do not assign a lambda expression, use a def 39 | "C901", 40 | ] 41 | 42 | exclude = ["tests/fake_client.py"] 43 | 44 | [lint.flake8-pytest-style] 45 | fixture-parentheses = false 46 | 47 | [lint.pyupgrade] 48 | keep-runtime-typing = true 49 | 50 | [lint.mccabe] 51 | max-complexity = 25 52 | -------------------------------------------------------------------------------- /custom_components/integration_ecowitt/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Support for Ecowitt Weather Stations.""" 2 | 3 | import logging 4 | 5 | from homeassistant.components.binary_sensor import BinarySensorEntity 6 | from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN 7 | from homeassistant.const import STATE_OFF 8 | from homeassistant.const import STATE_ON 9 | from homeassistant.const import STATE_UNKNOWN 10 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 11 | 12 | from . import async_add_ecowitt_entities 13 | from . import EcowittEntity 14 | from .const import DOMAIN 15 | from .const import REG_ENTITIES 16 | from .const import SIGNAL_ADD_ENTITIES 17 | from .const import TYPE_BINARY_SENSOR 18 | 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | 22 | async def async_setup_entry(hass, entry, async_add_entities): 23 | """Add sensors if new.""" 24 | 25 | async def add_entities(discovery_info=None): 26 | await async_add_ecowitt_entities( 27 | hass, 28 | entry, 29 | EcowittBinarySensor, 30 | BINARY_SENSOR_DOMAIN, 31 | async_add_entities, 32 | discovery_info, 33 | ) 34 | 35 | signal = f"{SIGNAL_ADD_ENTITIES}_{BINARY_SENSOR_DOMAIN}" 36 | async_dispatcher_connect(hass, signal, add_entities) 37 | await add_entities( 38 | hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][TYPE_BINARY_SENSOR] 39 | ) 40 | 41 | 42 | class EcowittBinarySensor(EcowittEntity, BinarySensorEntity): 43 | """Definition of a binary sensor.""" 44 | 45 | def __init__(self, hass, entry, key, name, dc, uom, icon, sc): 46 | """Initialize the sensor.""" 47 | super().__init__(hass, entry, key, name) 48 | self._icon = icon 49 | self._uom = uom 50 | self._dc = dc 51 | 52 | @property 53 | def is_on(self): 54 | """Return true if the binary sensor is on.""" 55 | if self._key in self._ws.last_values: 56 | if self._ws.last_values[self._key] > 0: 57 | return True 58 | else: 59 | _LOGGER.warning( 60 | "Sensor %s not in last update, check range or battery", self._key 61 | ) 62 | return None 63 | return False 64 | 65 | @property 66 | def state(self): 67 | """Return the state of the binary sensor.""" 68 | # Don't claim a leak is cleared if the sensor is out of range 69 | if self.is_on is None: 70 | return STATE_UNKNOWN 71 | return STATE_ON if self.is_on else STATE_OFF 72 | 73 | @property 74 | def device_class(self): 75 | """Return the device class.""" 76 | return self._dc 77 | -------------------------------------------------------------------------------- /custom_components/integration_ecowitt/sensor.py: -------------------------------------------------------------------------------- 1 | """Support for Ecowitt Weather Stations.""" 2 | 3 | import logging 4 | 5 | import homeassistant.util.dt as dt_util 6 | from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN 7 | from homeassistant.components.sensor import SensorDeviceClass 8 | from homeassistant.components.sensor import SensorEntity 9 | from homeassistant.const import PERCENTAGE 10 | from homeassistant.const import STATE_UNKNOWN 11 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 12 | 13 | from . import async_add_ecowitt_entities 14 | from . import EcowittEntity 15 | from .const import DOMAIN 16 | from .const import REG_ENTITIES 17 | from .const import SIGNAL_ADD_ENTITIES 18 | from .const import TYPE_SENSOR 19 | 20 | _LOGGER = logging.getLogger(__name__) 21 | 22 | 23 | async def async_setup_entry(hass, entry, async_add_entities): 24 | """Add sensors if new.""" 25 | 26 | async def add_entities(discovery_info=None): 27 | await async_add_ecowitt_entities( 28 | hass, 29 | entry, 30 | EcowittSensor, 31 | SENSOR_DOMAIN, 32 | async_add_entities, 33 | discovery_info, 34 | ) 35 | 36 | signal = f"{SIGNAL_ADD_ENTITIES}_{SENSOR_DOMAIN}" 37 | async_dispatcher_connect(hass, signal, add_entities) 38 | await add_entities(hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][TYPE_SENSOR]) 39 | 40 | 41 | class EcowittSensor(EcowittEntity, SensorEntity): 42 | """Definition of a sensor.""" 43 | 44 | def __init__(self, hass, entry, key, name, dc, uom, icon, sc): 45 | """Initialize the sensor.""" 46 | super().__init__(hass, entry, key, name) 47 | self._icon = icon 48 | self._uom = uom 49 | self._dc = dc 50 | self._sc = sc 51 | 52 | @property 53 | def native_value(self): 54 | """Return the state of the sensor.""" 55 | if self._key in self._ws.last_values: 56 | # The lightning time is reported in UTC, hooray. 57 | if self._dc == SensorDeviceClass.TIMESTAMP: 58 | if not isinstance(self._ws.last_values[self._key], int): 59 | return STATE_UNKNOWN 60 | return dt_util.as_local( 61 | dt_util.utc_from_timestamp(self._ws.last_values[self._key]) 62 | ).isoformat() 63 | # Battery value is 0-5 64 | if self._dc == SensorDeviceClass.BATTERY and self._uom == PERCENTAGE: 65 | return self._ws.last_values[self._key] * 20.0 66 | return self._ws.last_values[self._key] 67 | _LOGGER.warning( 68 | "Sensor %s not in last update, check range or battery", self._key 69 | ) 70 | return STATE_UNKNOWN 71 | 72 | @property 73 | def native_unit_of_measurement(self): 74 | """Return the unit of measurement.""" 75 | return self._uom 76 | 77 | @property 78 | def icon(self): 79 | """Return the icon to use in the fronend.""" 80 | return self._icon 81 | 82 | @property 83 | def device_class(self): 84 | """Return the device class.""" 85 | return self._dc 86 | 87 | @property 88 | def state_class(self): 89 | """Return sensor state class.""" 90 | return self._sc 91 | -------------------------------------------------------------------------------- /tests/fake_client.py: -------------------------------------------------------------------------------- 1 | """A bone-simple fake client used to test the hass integration.""" 2 | import http.client 3 | import time 4 | import urllib.parse 5 | from datetime import datetime 6 | from random import randrange 7 | 8 | MY_PASSKEY = "34271334ED1FADA6D8B988B14267E55D" 9 | # MY_PASSKEY = '35271334ED1FADA7D8B988B22222E22D' 10 | 11 | paramset_a = { 12 | "PASSKEY": MY_PASSKEY, 13 | "stationtype": "EasyWeatherV1.4.9", 14 | "dateutc": "2020-11-13+17:10:24", 15 | "tempinc": 20.0, 16 | "tempinf": 68.0, 17 | "humidityin": 40, 18 | "baromrelin": 28.760, 19 | "baromabsin": 28.760, 20 | "tempc": 20.0, 21 | "tempf": 68.0, 22 | "humidity": 64, 23 | "winddir": 319, 24 | "windspeedmph": 0.9, 25 | "windgustmph": 1.1, 26 | "rainratein": 0.000, 27 | "eventrainin": 0.000, 28 | "dailyrainin": 0.000, 29 | "weeklyrainin": 0.024, 30 | "monthlyrainin": 0.028, 31 | "yearlyrainin": 0.843, 32 | "solarradiation": 375.53, 33 | "uv": 3, 34 | "pm25_ch1": 8.0, 35 | "pm25_avg_24h_ch1": 5.2, 36 | "freq": "915M", 37 | "model": "HP3500_V1.6.2", 38 | "leak_ch1": 0, 39 | "leakbatt1": 5, 40 | } 41 | 42 | paramset_b = { 43 | "PASSKEY": MY_PASSKEY, 44 | "stationtype": "EasyWeatherV1.5.4", 45 | "dateutc": "2020-11-16+15:30:24", 46 | "tempinc": 20.7, 47 | "tempinf": 69.26, 48 | "humidityin": 52, 49 | "baromrelin": 29.785, 50 | "baromabsin": 29.785, 51 | "tempc": 20.4, 52 | "tempf": 68.72, 53 | "humidity": 94, 54 | "winddir": 260, 55 | "winddir_avg10m": 260, 56 | "windspeedmph": 0.0, 57 | "windspdmph_avg10m": 0.0, 58 | "windgustmph": 0.0, 59 | "maxdailygust": 6.9, 60 | "rainratein": 0.000, 61 | "eventrainin": 0.118, 62 | "hourlyrainin": 0.000, 63 | "dailyrainin": 0.118, 64 | "weeklyrainin": 0.118, 65 | "monthlyrainin": 0.378, 66 | "yearlyrainin": 6.268, 67 | "solarradiation": 0.00, 68 | "uv": 0, 69 | "soilmoisture1": 0, 70 | "wh65batt": 1, 71 | "wh25batt": 0, 72 | "soilbatt1": 1.5, 73 | "leak_ch1": 0, 74 | "leakbatt1": 5, 75 | "leak_ch2": 1, 76 | "leakbatt2": 3, 77 | "tf_co2": 56.7, 78 | "humi_co2": 72, 79 | "pm25_co2": 24.7, 80 | "pm25_24h_co2": 29.4, 81 | "pm10_co2": 24.7, 82 | "pm10_24h_co2": 29.9, 83 | "co2": 455, 84 | "co2_24h": 464, 85 | "co2_batt": 6, 86 | "freq": "868M", 87 | "model": "HP1000SE-PRO_Pro_V1.6.0", 88 | } 89 | 90 | host = "localhost" 91 | port = "4199" 92 | 93 | while True: 94 | try: 95 | print(f"Connecting to host {host} on port {port}") 96 | conn = http.client.HTTPConnection(host, port) 97 | headers = {"Content-type": "application/x-www-form-urlencoded"} 98 | 99 | usedParam = paramset_a 100 | 101 | usedParam["dateutc"] = datetime.now().strftime("%Y-%m-%d+%H:%M:%S") 102 | usedParam["tempinc"] = 20 + randrange(10) / 10.0 103 | 104 | params = urllib.parse.urlencode(usedParam) 105 | print(params) 106 | conn.request("POST", "", params, headers) 107 | response = conn.getresponse() 108 | print(response.status, response.reason) 109 | conn.close() 110 | except Exception as err: 111 | print(err) 112 | finally: 113 | time.sleep(5) 114 | -------------------------------------------------------------------------------- /custom_components/integration_ecowitt/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for ecowitt.""" 2 | 3 | import logging 4 | 5 | import voluptuous as vol 6 | from homeassistant import config_entries 7 | from homeassistant import core 8 | from homeassistant import exceptions 9 | from homeassistant.const import CONF_PORT 10 | from homeassistant.core import callback 11 | 12 | from .const import CONF_UNIT_BARO 13 | from .const import CONF_UNIT_LIGHTNING 14 | from .const import CONF_UNIT_RAIN 15 | from .const import CONF_UNIT_WIND 16 | from .const import CONF_UNIT_WINDCHILL 17 | from .const import DOMAIN 18 | from .const import UNIT_BARO_OPTS 19 | from .const import UNIT_LENGTH_OPTS 20 | from .const import UNIT_PRECI_OPTS 21 | from .const import UNIT_WIND_OPTS 22 | from .const import W_TYPE_HYBRID 23 | from .const import WINDCHILL_OPTS 24 | from .schemas import ( 25 | DATA_SCHEMA, 26 | ) 27 | 28 | 29 | _LOGGER = logging.getLogger(__name__) 30 | 31 | 32 | async def validate_input(hass: core.HomeAssistant, data): 33 | """Validate user input.""" 34 | for entry in hass.config_entries.async_entries(DOMAIN): 35 | if entry.data[CONF_PORT] == data[CONF_PORT]: 36 | raise AlreadyConfigured 37 | return {"title": f"Ecowitt on port {data[CONF_PORT]}"} 38 | 39 | 40 | class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 41 | """Config flow for the Ecowitt.""" 42 | 43 | VERSION = 1 44 | CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN 45 | 46 | async def async_step_import(self, device_config): 47 | """Import a configuration.yaml config, if any.""" 48 | try: 49 | await validate_input(self.hass, device_config) 50 | except AlreadyConfigured: 51 | return self.async_abort(reason="already_configured") 52 | 53 | port = device_config[CONF_PORT] 54 | return self.async_create_entry( 55 | title=f"Ecowitt on port {port}", data=device_config 56 | ) 57 | 58 | async def async_step_user(self, user_input=None): 59 | """Give initial instructions for setup.""" 60 | if user_input is not None: 61 | return await self.async_step_initial_options() 62 | 63 | return self.async_show_form(step_id="user") 64 | 65 | async def async_step_initial_options(self, user_input=None): 66 | """Ask the user for the setup options.""" 67 | errors = {} 68 | if user_input is not None: 69 | try: 70 | info = await validate_input(self.hass, user_input) 71 | return self.async_create_entry(title=info["title"], data=user_input) 72 | except AlreadyConfigured: 73 | return self.async_abort(reason="already_configured") 74 | 75 | return self.async_show_form( 76 | step_id="initial_options", data_schema=DATA_SCHEMA, errors=errors 77 | ) 78 | 79 | @staticmethod 80 | @callback 81 | def async_get_options_flow(config_entry): 82 | """Options flow.""" 83 | return EcowittOptionsFlowHandler(config_entry) 84 | 85 | 86 | class AlreadyConfigured(exceptions.HomeAssistantError): 87 | """Error to indicate this device is already configured.""" 88 | 89 | 90 | class EcowittOptionsFlowHandler(config_entries.OptionsFlow): 91 | """Ecowitt config flow options handler.""" 92 | 93 | def __init__(self, config_entry): 94 | """Initialize HASS options flow.""" 95 | self.config_entry = config_entry 96 | 97 | async def async_step_init(self, user_input=None): 98 | """Handle a flow initialized by the user.""" 99 | if user_input is not None: 100 | return self.async_create_entry(title="", data=user_input) 101 | 102 | options_schema = vol.Schema( 103 | { 104 | vol.Optional( 105 | CONF_UNIT_BARO, 106 | default=self.config_entry.options.get( 107 | CONF_UNIT_BARO, 108 | self.hass.config.units.pressure_unit, 109 | ), 110 | ): vol.In(UNIT_BARO_OPTS), 111 | vol.Optional( 112 | CONF_UNIT_WIND, 113 | default=self.config_entry.options.get( 114 | CONF_UNIT_WIND, 115 | self.hass.config.units.wind_speed_unit, 116 | ), 117 | ): vol.In(UNIT_WIND_OPTS), 118 | vol.Optional( 119 | CONF_UNIT_RAIN, 120 | default=self.config_entry.options.get( 121 | CONF_UNIT_RAIN, 122 | self.hass.config.units.accumulated_precipitation_unit, 123 | ), 124 | ): vol.In(UNIT_PRECI_OPTS), 125 | vol.Optional( 126 | CONF_UNIT_LIGHTNING, 127 | default=self.config_entry.options.get( 128 | CONF_UNIT_LIGHTNING, 129 | self.hass.config.units.length_unit, 130 | ), 131 | ): vol.In(UNIT_LENGTH_OPTS), 132 | vol.Optional( 133 | CONF_UNIT_WINDCHILL, 134 | default=self.config_entry.options.get( 135 | CONF_UNIT_WINDCHILL, 136 | W_TYPE_HYBRID, 137 | ), 138 | ): vol.In(WINDCHILL_OPTS), 139 | } 140 | ) 141 | return self.async_show_form(step_id="init", data_schema=options_schema) 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) 2 | 3 | # Ecowitt Weather Station integration for home-assistant 4 | 5 | Ecowitt driver for homeassistant 6 | 7 | ![Bling](https://raw.githubusercontent.com/garbled1/homeassistant_ecowitt/master/md.images/overview.png) 8 | 9 | ## Configuration: 10 | 11 | Configuration for the Ecowitt integration is now performed via a config flow 12 | as opposed to yaml configuration file. 13 | 14 | 1. Go to HACS -> Integrations -> Click "+" 15 | 1. Search for "Ecowitt Weather Station" repository and add to HACS 16 | 1. Restart Home Assistant when it says to. 17 | 1. In Home Assistant, go to Configuration -> Integrations -> Click "+ Add Integration" 18 | 1. Search for "Ecowitt Weather Station" and follow the instructions to setup. 19 | 20 | Ecowitt should now appear as a card under the HA Integrations page 21 | with "Options" selection available at the bottom of the card. 22 | 23 | You must select the port when enabling, see below section on "How to set up". 24 | 25 | There are a few options available once the integration is setup, these are 26 | available in the "options" dialog in the integrations box for the component. 27 | 28 | - Barometer Unit (default metric) 29 | - Wind Unit (default imperial) 30 | - Rainfall Unit (default imperial) 31 | - Lightning Unit (default imperial) 32 | - Windchill Unit (default hybrid) 33 | 34 | Windchill can be one of "hybrid", "old", or "new". 35 | Defaults for units are as shown above. 36 | Units can be one of "metric" or "imperial". 37 | 38 | Note that if you change the units, it will create a new sensor for the 39 | different unit. 40 | For example, if you had wind set to imperial, "sensor.wind_speed" 41 | would have been your data entity, but switching to metric will create a 42 | "sensor.wind_speed_2". 43 | You will see in the entities page the original "sensor.wind_speed" will be 44 | marked with a status of "Restored". 45 | You can safely delete the old sensor once you validate you are seeing data 46 | on the new one. 47 | Be sure to update any automations/etc that reference the old sensor. 48 | 49 | ### Breaking changes 50 | 51 | Version 0.5 converts this to a config flow from the previous yaml config method. 52 | Once you restart hass, it will attempt to read your old config from yaml, and 53 | port it over to the config flow. 54 | Verify that it did so correctly, and double check the options match what you 55 | expect for the units under "Options". 56 | 57 | Additionally in 0.5, the battery sensors have been significantly changed. 58 | Previously all batteries were simple floats with the raw value displayed. 59 | There are 3 types of batteries that the ecowitt displays data for: 60 | 61 | - Simple good/bad batteries. These are now binary sensors. This will leave 62 | A dead entry in your entities for the old battery sensor. You may safely 63 | delete that entity. 64 | - Voltage readings. A few batteries display a voltage (soil, WH80). 65 | A soil battery is normally 1.5v, so a good alarm might be around 1.3? 66 | WH80 batteries seem to be about 2.38 - 2.4, so maybe in the 2.3 to 2.2 range 67 | for an alarm? 68 | - Other batteries will now show as a percentage. 69 | The raw sensor gives a number from 0-5, this is simply multiplied by 20 70 | to give a percentage of 0-100. 71 | 72 | If you were monitoring one of these, be sure to update any automations. 73 | 74 | There was a bug in the wind gust sensors, where it was not being affected by 75 | the windunit setting, once you load 0.5, you may find a dead entity for your 76 | wind gust sensors that were setup for the wrong unit. 77 | You may delete these. 78 | 79 | Once your configuration has been moved, you should delete the old ecowitt 80 | section from your configuration.yaml file and restart hass. 81 | 82 | ## How to set up: 83 | 84 | Use the WS View app (on your phone) for your Ecowitt device, and connect to it. 85 | 86 | 1. Pick menu -> device list -> Pick your station. 87 | 1. Hit next repeatedly to move to the last screen titled "Customized" 88 | 1. Pick the protocol Ecowitt, and put in the ip/hostname of your hass server. 89 | 1. Path doesn't matter as long as it ends in /, leave the default, or change it to 90 | just /. 91 | 1. Pick a port that is not in use on the server (netstat -lt). 92 | (4199 is probably a good default) 93 | 1. Pick a reasonable value for updates, like 60 seconds. 94 | 1. Save configuration. 95 | 96 | The Ecowitt should then start attempting to send data to your server. 97 | 98 | In home assistant, navigate to integrations, and search for the ecowitt component. 99 | You will need to supply a port, and an optional name for the station (if you have 100 | multiple stations this might be useful) 101 | Pick the same port you did in the wsview app. 102 | 103 | One note: You may wish to setup the integration, and change the options for 104 | the various units prior to setting up the physical device. 105 | This will prevent creation of any entities for the wrong measurement unit from 106 | being created if you decide to change one to a non-default. 107 | 108 | ## Errors in the logs 109 | 110 | If you get an error in the logs about an unhandled sensor, open an issue and 111 | paste the log message so I can add the sensor. 112 | 113 | If you have a sensor that is barely in range, you will see a bunch of messages 114 | in the logs about the sensor not being in the most recent update. 115 | This can also be caused by a sensor that has a low battery. 116 | If you know this sensor is just badly placed, you can ignore these, but if you 117 | start seeing them for previously reliable sensors, check the batteries. 118 | 119 | ## Delay on startup 120 | 121 | Older versions of this component would cause homeassistant to freeze on startup 122 | waiting for the first sensor burst. 123 | This is no longer the case. 124 | Sensors will now show up as restored until the first data packet is recieved 125 | from the ecowitt. 126 | There should be no delay on startup at all. 127 | 128 | ## A note on leak sensors 129 | 130 | Because leak sensors may be potentially important devices for automation, 131 | they handle going out of range somewhat differently. 132 | If a leak sensor is missing from the last data update from the ecowitt, it 133 | will go into state Unknown. 134 | If you rely upon a leak sensor for something vital, I suggest testing your 135 | automation, by disconnecting the battery from the sensor, and validating 136 | your code does something sane. 137 | 138 | ## I want a pretty card for my weather 139 | 140 | I highly reccomend https://github.com/r-renato/ha-card-weather-conditions 141 | It's fairly simple to setup as a custom card, and produces lovely results. 142 | You can easily set it up to combine local data from your sensors, with 143 | forcast data from external sources for sensors you don't have 144 | (like pollen counts, for example). 145 | 146 | This is a copy of my setup. 147 | Sensors named with the sensor.cc\_ are from the climacell external source, 148 | other sensors are my local weatherstation. 149 | 150 | ``` 151 | air_quality: 152 | co: sensor.cc_co 153 | epa_aqi: sensor.cc_epa_aqi 154 | epa_health_concern: sensor.cc_epa_health_concern 155 | no2: sensor.cc_no2 156 | o3: sensor.cc_o3 157 | pm10: sensor.cc_pm10 158 | pm25: sensor.pm2_5_1 159 | so2: sensor.cc_so2 160 | animation: true 161 | name: WeatherStation 162 | pollen: 163 | grass: 164 | entity: sensor.cc_pollen_grass 165 | high: 3 166 | low: 1 167 | max: 5 168 | min: 0 169 | tree: 170 | entity: sensor.cc_pollen_tree 171 | high: 3 172 | low: 1 173 | max: 5 174 | min: 0 175 | weed: 176 | entity: sensor.cc_pollen_weed 177 | high: 3 178 | low: 1 179 | max: 5 180 | min: 0 181 | type: 'custom:ha-card-weather-conditions' 182 | weather: 183 | current: 184 | current_conditions: sensor.cc_weather_condition 185 | feels_like: sensor.windchill 186 | forecast: true 187 | humidity: sensor.humidity 188 | precipitation: sensor.rain_rate 189 | pressure: sensor.absolute_pressure 190 | sun: sun.sun 191 | temperature: sensor.outdoor_temperature 192 | visibility: sensor.cc_visibility 193 | wind_bearing: sensor.wind_direction 194 | wind_speed: sensor.wind_speed 195 | forecast: 196 | icons: 197 | day_1: sensor.cc_weather_condition_0d 198 | day_2: sensor.cc_weather_condition_1d 199 | day_3: sensor.cc_weather_condition_2d 200 | day_4: sensor.cc_weather_condition_3d 201 | day_5: sensor.cc_weather_condition_4d 202 | precipitation_intensity: 203 | day_1: sensor.cc_max_precipitation_0d 204 | day_2: sensor.cc_max_precipitation_1d 205 | day_3: sensor.cc_max_precipitation_2d 206 | day_4: sensor.cc_max_precipitation_3d 207 | day_5: sensor.cc_max_precipitation_4d 208 | precipitation_probability: 209 | day_1: sensor.cc_precipitation_probability_0d 210 | day_2: sensor.cc_precipitation_probability_1d 211 | day_3: sensor.cc_precipitation_probability_2d 212 | day_4: sensor.cc_precipitation_probability_3d 213 | day_5: sensor.cc_precipitation_probability_4d 214 | temperature_high: 215 | day_1: sensor.cc_max_temperature_0d 216 | day_2: sensor.cc_max_temperature_1d 217 | day_3: sensor.cc_max_temperature_2d 218 | day_4: sensor.cc_max_temperature_3d 219 | day_5: sensor.cc_max_temperature_4d 220 | temperature_low: 221 | day_1: sensor.cc_min_temperature_0d 222 | day_2: sensor.cc_min_temperature_1d 223 | day_3: sensor.cc_min_temperature_2d 224 | day_4: sensor.cc_min_temperature_3d 225 | day_5: sensor.cc_min_temperature_4d 226 | icons_model: climacell 227 | ``` 228 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /custom_components/integration_ecowitt/__init__.py: -------------------------------------------------------------------------------- 1 | """The Ecowitt Weather Station Component.""" 2 | 3 | import asyncio 4 | import logging 5 | import time 6 | 7 | from homeassistant.config_entries import ConfigEntry 8 | from homeassistant.config_entries import SOURCE_IMPORT 9 | from homeassistant.const import CONF_PORT 10 | from homeassistant.core import callback 11 | from homeassistant.core import HomeAssistant 12 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 13 | from homeassistant.helpers.dispatcher import async_dispatcher_send 14 | from homeassistant.helpers.entity import Entity 15 | from homeassistant.helpers.entity_registry import async_get 16 | from homeassistant.util.unit_system import METRIC_SYSTEM 17 | from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM 18 | from pyecowitt import EcoWittListener 19 | from pyecowitt import WINDCHILL_HYBRID 20 | from pyecowitt import WINDCHILL_NEW 21 | from pyecowitt import WINDCHILL_OLD 22 | 23 | from .const import CONF_NAME 24 | from .const import CONF_UNIT_BARO 25 | from .const import CONF_UNIT_LIGHTNING 26 | from .const import CONF_UNIT_RAIN 27 | from .const import CONF_UNIT_SYSTEM_METRIC_MS 28 | from .const import CONF_UNIT_WIND 29 | from .const import CONF_UNIT_WINDCHILL 30 | from .const import DATA_ECOWITT 31 | from .const import DATA_FREQ 32 | from .const import DATA_MODEL 33 | from .const import DATA_OPTIONS 34 | from .const import DATA_PASSKEY 35 | from .const import DATA_READY 36 | from .const import DATA_STATION 37 | from .const import DATA_STATIONTYPE 38 | from .const import DOMAIN 39 | from .const import ECOWITT_PLATFORMS 40 | from .const import IGNORED_SENSORS 41 | from .const import REG_ENTITIES 42 | from .const import S_IMPERIAL 43 | from .const import S_METRIC 44 | from .const import S_METRIC_MS 45 | from .const import SENSOR_TYPES 46 | from .const import SIGNAL_ADD_ENTITIES 47 | from .const import SIGNAL_REMOVE_ENTITIES 48 | from .const import TYPE_BINARY_SENSOR 49 | from .const import TYPE_SENSOR 50 | from .const import W_TYPE_HYBRID 51 | from .const import W_TYPE_NEW 52 | from .const import W_TYPE_OLD 53 | 54 | NOTIFICATION_ID = DOMAIN 55 | NOTIFICATION_TITLE = "Ecowitt config migrated" 56 | 57 | _LOGGER = logging.getLogger(__name__) 58 | 59 | 60 | async def async_setup(hass: HomeAssistant, config: dict): 61 | """Configure the Ecowitt component using YAML.""" 62 | hass.data.setdefault(DOMAIN, {}) 63 | 64 | if DOMAIN in config: 65 | data = { 66 | CONF_PORT: config[DOMAIN][CONF_PORT], 67 | CONF_NAME: None, 68 | } 69 | # set defaults if not set in conf 70 | if CONF_UNIT_BARO not in config[DOMAIN]: 71 | config[DOMAIN][CONF_UNIT_BARO] = hass.config.units.pressure_unit 72 | if CONF_UNIT_WIND not in config[DOMAIN]: 73 | config[DOMAIN][CONF_UNIT_WIND] = hass.config.units.wind_speed_unit 74 | if CONF_UNIT_RAIN not in config[DOMAIN]: 75 | config[DOMAIN][CONF_UNIT_RAIN] = ( 76 | hass.config.units.accumulated_precipitation_unit 77 | ) 78 | if CONF_UNIT_LIGHTNING not in config[DOMAIN]: 79 | config[DOMAIN][CONF_UNIT_LIGHTNING] = hass.config.units.length_unit 80 | if CONF_UNIT_WINDCHILL not in config[DOMAIN]: 81 | config[DOMAIN][CONF_UNIT_WINDCHILL] = W_TYPE_HYBRID 82 | # set the options for migration 83 | hass.data[DOMAIN][DATA_OPTIONS] = { 84 | CONF_UNIT_BARO: config[DOMAIN][CONF_UNIT_BARO], 85 | CONF_UNIT_WIND: config[DOMAIN][CONF_UNIT_WIND], 86 | CONF_UNIT_RAIN: config[DOMAIN][CONF_UNIT_RAIN], 87 | CONF_UNIT_LIGHTNING: config[DOMAIN][CONF_UNIT_LIGHTNING], 88 | CONF_UNIT_WINDCHILL: config[DOMAIN][CONF_UNIT_WINDCHILL], 89 | } 90 | hass.components.persistent_notification.create( 91 | "Ecowitt configuration has been migrated from yaml format " 92 | "to a config_flow. Your options and settings should have been " 93 | "migrated automatically. Verify them in the Configuration -> " 94 | "Integrations menu, and then delete the ecowitt section from " 95 | "your yaml file.", 96 | title=NOTIFICATION_TITLE, 97 | notification_id=NOTIFICATION_ID, 98 | ) 99 | hass.async_create_task( 100 | hass.config_entries.flow.async_init( 101 | DOMAIN, context={"source": SOURCE_IMPORT}, data=data 102 | ) 103 | ) 104 | return True 105 | 106 | 107 | async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry): 108 | """Set up the Ecowitt component from UI.""" 109 | 110 | 111 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 112 | """Set up the Ecowitt component from UI.""" 113 | 114 | if hass.data.get(DOMAIN) is None: 115 | hass.data.setdefault(DOMAIN, {}) 116 | 117 | # if options existed in the YAML but not in the config entry, add 118 | if ( 119 | not entry.options 120 | and entry.source == SOURCE_IMPORT 121 | and hass.data.get(DOMAIN) 122 | and hass.data[DOMAIN].get(DATA_OPTIONS) 123 | ): 124 | hass.config_entries.async_update_entry( 125 | entry=entry, 126 | options=hass.data[DOMAIN][DATA_OPTIONS], 127 | ) 128 | 129 | # Store config 130 | hass.data[DOMAIN][entry.entry_id] = {} 131 | ecowitt_data = hass.data[DOMAIN][entry.entry_id] 132 | ecowitt_data[DATA_STATION] = {} 133 | ecowitt_data[DATA_READY] = False 134 | ecowitt_data[REG_ENTITIES] = {} 135 | for pl in ECOWITT_PLATFORMS: 136 | ecowitt_data[REG_ENTITIES][pl] = [] 137 | 138 | if not entry.options: 139 | hass.config_entries.async_update_entry( 140 | entry=entry, 141 | options={ 142 | CONF_UNIT_BARO: hass.config.units.pressure_unit, 143 | CONF_UNIT_WIND: hass.config.units.wind_speed_unit, 144 | CONF_UNIT_RAIN: hass.config.units.accumulated_precipitation_unit, 145 | CONF_UNIT_LIGHTNING: hass.config.units.length_unit, 146 | CONF_UNIT_WINDCHILL: W_TYPE_HYBRID, 147 | }, 148 | ) 149 | 150 | # preload some model info 151 | stationinfo = ecowitt_data[DATA_STATION] 152 | stationinfo[DATA_STATIONTYPE] = "Unknown" 153 | stationinfo[DATA_FREQ] = "Unknown" 154 | stationinfo[DATA_MODEL] = "Unknown" 155 | 156 | # setup the base connection 157 | web_server = EcoWittListener(port=entry.data[CONF_PORT]) 158 | ecowitt_data[DATA_ECOWITT] = web_server 159 | 160 | if entry.options[CONF_UNIT_WINDCHILL] == W_TYPE_OLD: 161 | web_server.set_windchill(WINDCHILL_OLD) 162 | if entry.options[CONF_UNIT_WINDCHILL] == W_TYPE_NEW: 163 | web_server.set_windchill(WINDCHILL_NEW) 164 | if entry.options[CONF_UNIT_WINDCHILL] == W_TYPE_HYBRID: 165 | web_server.set_windchill(WINDCHILL_HYBRID) 166 | 167 | hass.loop.create_task(web_server.listen()) 168 | 169 | async def close_server(*args): 170 | """Close the ecowitt server.""" 171 | await web_server.stop() 172 | 173 | def check_imp_metric_sensor(sensor): 174 | """Check if this is the wrong sensor for our config (imp/metric).""" 175 | # Is this a metric or imperial sensor, lookup and skip 176 | _, _, _, _, _, metric, _ = SENSOR_TYPES[sensor] 177 | if metric == 0: 178 | return True 179 | if "baro" in sensor: 180 | if ( 181 | entry.options[CONF_UNIT_BARO] == US_CUSTOMARY_SYSTEM.pressure_unit 182 | and metric == S_METRIC 183 | ): 184 | return False 185 | if ( 186 | entry.options[CONF_UNIT_BARO] == METRIC_SYSTEM.pressure_unit 187 | and metric == S_IMPERIAL 188 | ): 189 | return False 190 | if "rain" in sensor: 191 | if ( 192 | entry.options[CONF_UNIT_RAIN] 193 | == US_CUSTOMARY_SYSTEM.accumulated_precipitation_unit 194 | and metric == S_METRIC 195 | ): 196 | return False 197 | if ( 198 | entry.options[CONF_UNIT_RAIN] 199 | == METRIC_SYSTEM.accumulated_precipitation_unit 200 | and metric == S_IMPERIAL 201 | ): 202 | return False 203 | if "windchill" not in sensor and ("wind" in sensor or "gust" in sensor): 204 | if ( 205 | entry.options[CONF_UNIT_WIND] == US_CUSTOMARY_SYSTEM.wind_speed_unit 206 | and metric != S_IMPERIAL 207 | ): 208 | return False 209 | if ( 210 | entry.options[CONF_UNIT_WIND] == METRIC_SYSTEM.wind_speed_unit 211 | and metric != S_METRIC 212 | ): 213 | return False 214 | if ( 215 | entry.options[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_METRIC_MS 216 | and metric != S_METRIC_MS 217 | ): 218 | return False 219 | if ( 220 | sensor == "lightning" 221 | and entry.options[CONF_UNIT_LIGHTNING] == METRIC_SYSTEM.length_unit 222 | ): 223 | return False 224 | if ( 225 | sensor == "lightning_mi" 226 | and entry.options[CONF_UNIT_LIGHTNING] == US_CUSTOMARY_SYSTEM.length_unit 227 | ): 228 | return False 229 | return True 230 | 231 | def check_and_append_sensor(sensor): 232 | """Check the sensor for validity, and append to new entitiy list.""" 233 | if sensor not in SENSOR_TYPES: 234 | if sensor not in IGNORED_SENSORS: 235 | _LOGGER.warning("Unhandled sensor type %s", sensor) 236 | return None 237 | 238 | # Is this a metric or imperial sensor, lookup and skip 239 | if not check_imp_metric_sensor(sensor): 240 | return None 241 | 242 | _, _, kind, _, _, _, _ = SENSOR_TYPES[sensor] 243 | ecowitt_data[REG_ENTITIES][kind].append(sensor) 244 | return kind 245 | 246 | async def _first_data_rec(): 247 | _LOGGER.info("First ecowitt data recd, setting up sensors.") 248 | # check if we have model info, etc. 249 | if DATA_PASSKEY in web_server.last_values: 250 | stationinfo[DATA_PASSKEY] = web_server.last_values[DATA_PASSKEY] 251 | web_server.last_values.pop(DATA_PASSKEY, None) 252 | else: 253 | _LOGGER.error("No passkey, cannot set unique id.") 254 | return False 255 | if DATA_STATIONTYPE in web_server.last_values: 256 | stationinfo[DATA_STATIONTYPE] = web_server.last_values[DATA_STATIONTYPE] 257 | web_server.last_values.pop(DATA_STATIONTYPE, None) 258 | if DATA_FREQ in web_server.last_values: 259 | stationinfo[DATA_FREQ] = web_server.last_values[DATA_FREQ] 260 | web_server.last_values.pop(DATA_FREQ, None) 261 | if DATA_MODEL in web_server.last_values: 262 | stationinfo[DATA_MODEL] = web_server.last_values[DATA_MODEL] 263 | web_server.last_values.pop(DATA_MODEL, None) 264 | 265 | # load the sensors we have 266 | for sensor in web_server.last_values: 267 | check_and_append_sensor(sensor) 268 | 269 | if ( 270 | not ecowitt_data[REG_ENTITIES][TYPE_SENSOR] 271 | and not ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR] 272 | ): 273 | _LOGGER.error("No sensors found to monitor, check device config.") 274 | return False 275 | 276 | # for component in ECOWITT_PLATFORMS: 277 | # hass.async_create_task( 278 | # await hass.config_entries.async_forward_entry_setups(entry, component) 279 | # ) 280 | await hass.config_entries.async_forward_entry_setups(entry, ECOWITT_PLATFORMS) 281 | 282 | ecowitt_data[DATA_READY] = True 283 | 284 | async def _async_ecowitt_update_cb(weather_data): 285 | """Primary update callback called from pyecowitt.""" 286 | _LOGGER.debug("Primary update callback triggered.") 287 | 288 | new_sensors = {} 289 | old_sensors = [] 290 | for component in ECOWITT_PLATFORMS: 291 | new_sensors[component] = [] 292 | 293 | if not hass.data[DOMAIN][entry.entry_id][DATA_READY]: 294 | await _first_data_rec() 295 | return 296 | for sensor in weather_data: 297 | if sensor not in SENSOR_TYPES: 298 | if sensor not in IGNORED_SENSORS: 299 | _LOGGER.warning( 300 | "Unhandled sensor type %s value %s, " + "file a PR.", 301 | sensor, 302 | weather_data[sensor], 303 | ) 304 | elif ( 305 | sensor not in ecowitt_data[REG_ENTITIES][TYPE_SENSOR] 306 | and sensor not in ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR] 307 | and sensor not in IGNORED_SENSORS 308 | and check_imp_metric_sensor(sensor) 309 | ): 310 | _LOGGER.warning( 311 | "Unregistered sensor type %s value %s received.", 312 | sensor, 313 | weather_data[sensor], 314 | ) 315 | # try to register the sensor 316 | kind = check_and_append_sensor(sensor) 317 | if kind is not None: 318 | new_sensors[kind].append(sensor) 319 | # It's a sensor we know, not ignored, and of the wrong metricness 320 | elif ( 321 | ( 322 | sensor in ecowitt_data[REG_ENTITIES][TYPE_SENSOR] 323 | or sensor in ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR] 324 | ) 325 | and sensor not in IGNORED_SENSORS 326 | and not check_imp_metric_sensor(sensor) 327 | ): 328 | _LOGGER.warning("Removing sensor type %s.", sensor) 329 | old_sensors.append(sensor) 330 | 331 | # If we have old sensors, delete them. 332 | if old_sensors: 333 | await async_remove_ecowitt_entities(old_sensors, hass, ecowitt_data) 334 | 335 | # if we have new sensors, set them up. 336 | for component in ECOWITT_PLATFORMS: 337 | if new_sensors[component]: 338 | signal = f"{SIGNAL_ADD_ENTITIES}_{component}" 339 | async_dispatcher_send(hass, signal, new_sensors[component]) 340 | async_dispatcher_send(hass, DOMAIN) 341 | 342 | # this is part of the base async_setup_entry 343 | web_server.register_listener(_async_ecowitt_update_cb) 344 | return True 345 | 346 | 347 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): 348 | """Unload a config entry.""" 349 | 350 | web_server = hass.data[DOMAIN][entry.entry_id][DATA_ECOWITT] 351 | await web_server.stop() 352 | 353 | unload_ok = all( 354 | await asyncio.gather( 355 | *[ 356 | hass.config_entries.async_forward_entry_unload(entry, component) 357 | for component in ECOWITT_PLATFORMS 358 | ] 359 | ) 360 | ) 361 | if unload_ok: 362 | hass.data[DOMAIN].pop(entry.entry_id) 363 | 364 | return unload_ok 365 | 366 | 367 | async def async_remove_ecowitt_entities(entities, hass, ecowitt_data): 368 | """Remove a sensor if needed.""" 369 | 370 | try: 371 | eventData = {} 372 | for entity in entities: 373 | _, _, kind, _, _, _, _ = SENSOR_TYPES[entity] 374 | 375 | eventData[entity] = kind 376 | ecowitt_data[REG_ENTITIES][kind].remove(entity) 377 | 378 | async_dispatcher_send(hass, SIGNAL_REMOVE_ENTITIES, eventData) 379 | 380 | except Exception as e: 381 | _LOGGER.error(e) 382 | 383 | 384 | async def async_add_ecowitt_entities( 385 | hass, entry, entity_type, platform, async_add_entities, discovery_info 386 | ): 387 | """Add an ecowitt entities.""" 388 | entities = [] 389 | if discovery_info is None: 390 | return 391 | 392 | for new_entity in discovery_info: 393 | if new_entity not in hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][platform]: 394 | hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][platform].append(new_entity) 395 | name, uom, _, device_class, icon, _, state_class = SENSOR_TYPES[new_entity] 396 | 397 | new_hass_entity = entity_type( 398 | hass, entry, new_entity, name, device_class, uom, icon, state_class 399 | ) 400 | 401 | async_dispatcher_connect( 402 | hass, SIGNAL_REMOVE_ENTITIES, new_hass_entity.remove_entity 403 | ) 404 | 405 | entities.append(new_hass_entity) 406 | 407 | if entities: 408 | async_add_entities(entities, True) 409 | 410 | 411 | class EcowittEntity(Entity): 412 | """Base class for Ecowitt Weather Station.""" 413 | 414 | def __init__(self, hass, entry, key, name): 415 | """Construct the entity.""" 416 | self.hass = hass 417 | self._key = key 418 | self._name = name 419 | self._stationinfo = hass.data[DOMAIN][entry.entry_id][DATA_STATION] 420 | self._ws = hass.data[DOMAIN][entry.entry_id][DATA_ECOWITT] 421 | self._entry = entry 422 | 423 | @property 424 | def should_poll(self): 425 | """Ecowitt is a push.""" 426 | return False 427 | 428 | @property 429 | def unique_id(self): 430 | """Return a unique ID for this sensor.""" 431 | return f"{self._stationinfo[DATA_PASSKEY]}-{self._key}" 432 | 433 | @property 434 | def name(self): 435 | """Return the name of the sensor.""" 436 | return self._name 437 | 438 | @property 439 | def device_info(self): 440 | """Return device information for this sensor.""" 441 | if ( 442 | self._entry.data[CONF_NAME] != "" 443 | and self._entry.data[CONF_NAME] is not None 444 | ): 445 | dname = self._entry.data[CONF_NAME] 446 | else: 447 | dname = DOMAIN 448 | 449 | return { 450 | "identifiers": {(DOMAIN, self._stationinfo[DATA_PASSKEY])}, 451 | "name": dname, 452 | "manufacturer": DOMAIN, 453 | "model": self._stationinfo[DATA_MODEL], 454 | "sw_version": self._stationinfo[DATA_STATIONTYPE], 455 | # "via_device": (DOMAIN, self._stationinfo[DATA_STATIONTYPE]), 456 | # "frequency": self._stationinfo[DATA_FREQ], 457 | } 458 | 459 | async def async_added_to_hass(self): 460 | """Add an listener.""" 461 | async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) 462 | 463 | @callback 464 | async def remove_entity(self, discovery_info=None): 465 | """Remove an entity.""" 466 | 467 | if self._key in discovery_info: 468 | registry = async_get(self.hass) 469 | 470 | entity_id = registry.async_get_entity_id( 471 | discovery_info[self._key], DOMAIN, self.unique_id 472 | ) 473 | 474 | _LOGGER.debug( 475 | f"Found entity {entity_id} for key {self._key} -> Uniqueid: {self.unique_id}" 476 | ) 477 | if entity_id: 478 | registry.async_remove(entity_id) 479 | 480 | @callback 481 | def _update_callback(self) -> None: 482 | """Call from dispatcher when state changes.""" 483 | self.async_schedule_update_ha_state(force_refresh=True) 484 | 485 | @property 486 | def assumed_state(self) -> bool: 487 | """Return whether the state is based on actual reading from device.""" 488 | if (self._ws.lastupd + 5 * 60) < time.time(): 489 | return True 490 | return False 491 | -------------------------------------------------------------------------------- /custom_components/integration_ecowitt/const.py: -------------------------------------------------------------------------------- 1 | """Constants used by ecowitt component.""" 2 | 3 | from homeassistant.components.sensor import SensorDeviceClass 4 | from homeassistant.components.sensor import SensorStateClass 5 | from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER 6 | from homeassistant.const import CONCENTRATION_PARTS_PER_MILLION 7 | from homeassistant.const import DEGREE 8 | from homeassistant.const import UnitOfElectricPotential 9 | from homeassistant.const import UnitOfLength 10 | from homeassistant.const import PERCENTAGE 11 | from homeassistant.const import UnitOfPower 12 | from homeassistant.const import UnitOfPressure 13 | from homeassistant.const import UnitOfSpeed 14 | from homeassistant.const import UnitOfTemperature 15 | from homeassistant.const import UnitOfTime 16 | from homeassistant.const import UV_INDEX 17 | from homeassistant.util.unit_system import METRIC_SYSTEM 18 | from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM 19 | 20 | ECOWITT_PLATFORMS = ["sensor", "binary_sensor"] 21 | 22 | TYPE_SENSOR = "sensor" 23 | TYPE_BINARY_SENSOR = "binary_sensor" 24 | DOMAIN = "integration_ecowitt" 25 | DATA_CONFIG = "config" 26 | DATA_OPTIONS = "options" 27 | DATA_ECOWITT = "ecowitt_listener" 28 | DATA_STATION = "station" 29 | DATA_PASSKEY = "PASSKEY" 30 | DATA_STATIONTYPE = "stationtype" 31 | DATA_FREQ = "freq" 32 | DATA_MODEL = "model" 33 | DATA_READY = "ready" 34 | REG_ENTITIES = "registered" 35 | 36 | DEFAULT_PORT = 4199 37 | 38 | SIGNAL_ADD_ENTITIES = "ecowitt_add_entities" 39 | SIGNAL_REMOVE_ENTITIES = "ecowitt_remove_entities" 40 | 41 | CONF_NAME = "component_name" 42 | CONF_UNIT_BARO = "barounit" 43 | CONF_UNIT_WIND = "windunit" 44 | CONF_UNIT_RAIN = "rainunit" 45 | CONF_UNIT_WINDCHILL = "windchillunit" 46 | CONF_UNIT_LIGHTNING = "lightningunit" 47 | 48 | TYPE_BAROMABSHPA = "baromabshpa" 49 | TYPE_BAROMRELHPA = "baromrelhpa" 50 | TYPE_BAROMABSIN = "baromabsin" 51 | TYPE_BAROMRELIN = "baromrelin" 52 | TYPE_RAINRATEIN = "rainratein" 53 | TYPE_EVENTRAININ = "eventrainin" 54 | TYPE_HOURLYRAININ = "hourlyrainin" 55 | TYPE_TOTALRAININ = "totalrainin" 56 | TYPE_DAILYRAININ = "dailyrainin" 57 | TYPE_WEEKLYRAININ = "weeklyrainin" 58 | TYPE_MONTHLYRAININ = "monthlyrainin" 59 | TYPE_YEARLYRAININ = "yearlyrainin" 60 | TYPE_RAINRATEMM = "rainratemm" 61 | TYPE_EVENTRAINMM = "eventrainmm" 62 | TYPE_HOURLYRAINMM = "hourlyrainmm" 63 | TYPE_TOTALRAINMM = "totalrainmm" 64 | TYPE_DAILYRAINMM = "dailyrainmm" 65 | TYPE_WEEKLYRAINMM = "weeklyrainmm" 66 | TYPE_MONTHLYRAINMM = "monthlyrainmm" 67 | TYPE_YEARLYRAINMM = "yearlyrainmm" 68 | TYPE_HUMIDITY = "humidity" 69 | TYPE_HUMIDITY1 = "humidity1" 70 | TYPE_HUMIDITY2 = "humidity2" 71 | TYPE_HUMIDITY3 = "humidity3" 72 | TYPE_HUMIDITY4 = "humidity4" 73 | TYPE_HUMIDITY5 = "humidity5" 74 | TYPE_HUMIDITY6 = "humidity6" 75 | TYPE_HUMIDITY7 = "humidity7" 76 | TYPE_HUMIDITY8 = "humidity8" 77 | TYPE_HUMIDITYIN = "humidityin" 78 | TYPE_WINDDIR = "winddir" 79 | TYPE_WINDDIR_A10 = "winddir_avg10m" 80 | TYPE_WINDSPEEDKMH = "windspeedkmh" 81 | TYPE_WINDSPEEDKMH_A10 = "windspdkmh_avg10m" 82 | TYPE_WINDGUSTKMH = "windgustkmh" 83 | TYPE_WINDSPEEDMPH = "windspeedmph" 84 | TYPE_WINDSPEEDMPH_A10 = "windspdmph_avg10m" 85 | TYPE_WINDGUSTMPH = "windgustmph" 86 | TYPE_MAXDAILYGUST = "maxdailygust" 87 | TYPE_MAXDAILYGUSTKMH = "maxdailygustkmh" 88 | TYPE_WINDGUSTMS = "windgustms" 89 | TYPE_WINDSPEEDMS = "windspeedms" 90 | TYPE_WINDSPEEDMS_A10 = "windspdms_avg10m" 91 | TYPE_MAXDAILYGUSTMS = "maxdailygustms" 92 | TYPE_TEMPC = "tempc" 93 | TYPE_TEMPINC = "tempinc" 94 | TYPE_TEMP1C = "temp1c" 95 | TYPE_TEMP2C = "temp2c" 96 | TYPE_TEMP3C = "temp3c" 97 | TYPE_TEMP4C = "temp4c" 98 | TYPE_TEMP5C = "temp5c" 99 | TYPE_TEMP6C = "temp6c" 100 | TYPE_TEMP7C = "temp7c" 101 | TYPE_TEMP8C = "temp8c" 102 | TYPE_DEWPOINTC = "dewpointc" 103 | TYPE_DEWPOINTINC = "dewpointinc" 104 | TYPE_DEWPOINT1C = "dewpoint1c" 105 | TYPE_DEWPOINT2C = "dewpoint2c" 106 | TYPE_DEWPOINT3C = "dewpoint3c" 107 | TYPE_DEWPOINT4C = "dewpoint4c" 108 | TYPE_DEWPOINT5C = "dewpoint5c" 109 | TYPE_DEWPOINT6C = "dewpoint6c" 110 | TYPE_DEWPOINT7C = "dewpoint7c" 111 | TYPE_DEWPOINT8C = "dewpoint8c" 112 | TYPE_WINDCHILLC = "windchillc" 113 | TYPE_SOLARRADIATION = "solarradiation" 114 | TYPE_UV = "uv" 115 | TYPE_SOILMOISTURE1 = "soilmoisture1" 116 | TYPE_SOILMOISTURE2 = "soilmoisture2" 117 | TYPE_SOILMOISTURE3 = "soilmoisture3" 118 | TYPE_SOILMOISTURE4 = "soilmoisture4" 119 | TYPE_SOILMOISTURE5 = "soilmoisture5" 120 | TYPE_SOILMOISTURE6 = "soilmoisture6" 121 | TYPE_SOILMOISTURE7 = "soilmoisture7" 122 | TYPE_SOILMOISTURE8 = "soilmoisture8" 123 | TYPE_PM25_CH1 = "pm25_ch1" 124 | TYPE_PM25_CH2 = "pm25_ch2" 125 | TYPE_PM25_CH3 = "pm25_ch3" 126 | TYPE_PM25_CH4 = "pm25_ch4" 127 | TYPE_PM25_AVG_24H_CH1 = "pm25_avg_24h_ch1" 128 | TYPE_PM25_AVG_24H_CH2 = "pm25_avg_24h_ch2" 129 | TYPE_PM25_AVG_24H_CH3 = "pm25_avg_24h_ch3" 130 | TYPE_PM25_AVG_24H_CH4 = "pm25_avg_24h_ch4" 131 | TYPE_LIGHTNING_TIME = "lightning_time" 132 | TYPE_LIGHTNING_NUM = "lightning_num" 133 | TYPE_LIGHTNING_KM = "lightning" 134 | TYPE_LIGHTNING_MI = "lightning_mi" 135 | TYPE_CO2_TEMP = "tf_co2" 136 | TYPE_CO2_TEMPC = "tf_co2c" 137 | TYPE_CO2_HUMIDITY = "humi_co2" 138 | TYPE_CO2_PM25 = "pm25_co2" 139 | TYPE_CO2_PM25_AVG_24H = "pm25_24h_co2" 140 | TYPE_CO2_PM10 = "pm10_co2" 141 | TYPE_CO2_PM10_AVG_24H = "pm10_24h_co2" 142 | TYPE_CO2_CO2 = "co2" 143 | TYPE_CO2_CO2_AVG_24H = "co2_24h" 144 | TYPE_CO2_BATT = "co2_batt" 145 | TYPE_LEAK_CH1 = "leak_ch1" 146 | TYPE_LEAK_CH2 = "leak_ch2" 147 | TYPE_LEAK_CH3 = "leak_ch3" 148 | TYPE_LEAK_CH4 = "leak_ch4" 149 | TYPE_WH25BATT = "wh25batt" 150 | TYPE_WH26BATT = "wh26batt" 151 | TYPE_WH40BATT = "wh40batt" 152 | TYPE_WH57BATT = "wh57batt" 153 | TYPE_WH68BATT = "wh68batt" 154 | TYPE_WH65BATT = "wh65batt" 155 | TYPE_WH80BATT = "wh80batt" 156 | TYPE_SOILBATT1 = "soilbatt1" 157 | TYPE_SOILBATT2 = "soilbatt2" 158 | TYPE_SOILBATT3 = "soilbatt3" 159 | TYPE_SOILBATT4 = "soilbatt4" 160 | TYPE_SOILBATT5 = "soilbatt5" 161 | TYPE_SOILBATT6 = "soilbatt6" 162 | TYPE_SOILBATT7 = "soilbatt7" 163 | TYPE_SOILBATT8 = "soilbatt8" 164 | TYPE_BATTERY1 = "batt1" 165 | TYPE_BATTERY2 = "batt2" 166 | TYPE_BATTERY3 = "batt3" 167 | TYPE_BATTERY4 = "batt4" 168 | TYPE_BATTERY5 = "batt5" 169 | TYPE_BATTERY6 = "batt6" 170 | TYPE_BATTERY7 = "batt7" 171 | TYPE_BATTERY8 = "batt8" 172 | TYPE_PM25BATT1 = "pm25batt1" 173 | TYPE_PM25BATT2 = "pm25batt2" 174 | TYPE_PM25BATT3 = "pm25batt3" 175 | TYPE_PM25BATT4 = "pm25batt4" 176 | TYPE_PM25BATT5 = "pm25batt5" 177 | TYPE_PM25BATT6 = "pm25batt6" 178 | TYPE_PM25BATT7 = "pm25batt7" 179 | TYPE_PM25BATT8 = "pm25batt8" 180 | TYPE_LEAKBATT1 = "leakbatt1" 181 | TYPE_LEAKBATT2 = "leakbatt2" 182 | TYPE_LEAKBATT3 = "leakbatt3" 183 | TYPE_LEAKBATT4 = "leakbatt4" 184 | TYPE_LEAKBATT5 = "leakbatt5" 185 | TYPE_LEAKBATT6 = "leakbatt6" 186 | TYPE_LEAKBATT7 = "leakbatt7" 187 | TYPE_LEAKBATT8 = "leakbatt8" 188 | TYPE_WN34TEMP1C = "tf_ch1c" 189 | TYPE_WN34TEMP2C = "tf_ch2c" 190 | TYPE_WN34TEMP3C = "tf_ch3c" 191 | TYPE_WN34TEMP4C = "tf_ch4c" 192 | TYPE_WN34TEMP5C = "tf_ch5c" 193 | TYPE_WN34TEMP6C = "tf_ch6c" 194 | TYPE_WN34TEMP7C = "tf_ch7c" 195 | TYPE_WN34TEMP8C = "tf_ch8c" 196 | TYPE_WN34BATT1 = "tf_batt1" 197 | TYPE_WN34BATT2 = "tf_batt2" 198 | TYPE_WN34BATT3 = "tf_batt3" 199 | TYPE_WN34BATT4 = "tf_batt4" 200 | TYPE_WN34BATT5 = "tf_batt5" 201 | TYPE_WN34BATT6 = "tf_batt6" 202 | TYPE_WN34BATT7 = "tf_batt7" 203 | TYPE_WN34BATT8 = "tf_batt8" 204 | TYPE_VPD = "vpd" 205 | 206 | S_METRIC = 1 207 | S_IMPERIAL = 2 208 | S_METRIC_MS = 3 209 | 210 | W_TYPE_NEW = "new" 211 | W_TYPE_OLD = "old" 212 | W_TYPE_HYBRID = "hybrid" 213 | CONF_UNIT_SYSTEM_METRIC_MS = "metric_ms" 214 | 215 | LEAK_DETECTED = "Leak Detected" 216 | 217 | UNIT_PRECI_OPTS = [ 218 | METRIC_SYSTEM.accumulated_precipitation_unit, 219 | US_CUSTOMARY_SYSTEM.accumulated_precipitation_unit, 220 | ] 221 | 222 | 223 | UNIT_LENGTH_OPTS = [METRIC_SYSTEM.length_unit, US_CUSTOMARY_SYSTEM.length_unit] 224 | 225 | UNIT_BARO_OPTS = [METRIC_SYSTEM.pressure_unit, US_CUSTOMARY_SYSTEM.pressure_unit] 226 | UNIT_WIND_OPTS = [ 227 | METRIC_SYSTEM.wind_speed_unit, 228 | US_CUSTOMARY_SYSTEM.wind_speed_unit, 229 | CONF_UNIT_SYSTEM_METRIC_MS, 230 | ] 231 | WINDCHILL_OPTS = [W_TYPE_HYBRID, W_TYPE_NEW, W_TYPE_OLD] 232 | 233 | 234 | # Name, unit_of_measure, type, device_class, icon, metric=1, state_class 235 | # name, uom, kind, device_class, icon, metric, state_class = SENSOR_TYPES[x] 236 | SENSOR_TYPES = { 237 | TYPE_BAROMABSHPA: ( 238 | "Absolute Pressure", 239 | UnitOfPressure.HPA, 240 | TYPE_SENSOR, 241 | SensorDeviceClass.PRESSURE, 242 | "mdi:gauge", 243 | S_METRIC, 244 | SensorStateClass.MEASUREMENT, 245 | ), 246 | TYPE_BAROMRELHPA: ( 247 | "Relative Pressure", 248 | UnitOfPressure.HPA, 249 | TYPE_SENSOR, 250 | SensorDeviceClass.PRESSURE, 251 | "mdi:gauge", 252 | S_METRIC, 253 | SensorStateClass.MEASUREMENT, 254 | ), 255 | TYPE_BAROMABSIN: ( 256 | "Absolute Pressure", 257 | UnitOfPressure.INHG, 258 | TYPE_SENSOR, 259 | SensorDeviceClass.PRESSURE, 260 | "mdi:gauge", 261 | S_IMPERIAL, 262 | SensorStateClass.MEASUREMENT, 263 | ), 264 | TYPE_BAROMRELIN: ( 265 | "Relative Pressure", 266 | UnitOfPressure.INHG, 267 | TYPE_SENSOR, 268 | SensorDeviceClass.PRESSURE, 269 | "mdi:gauge", 270 | S_IMPERIAL, 271 | SensorStateClass.MEASUREMENT, 272 | ), 273 | TYPE_RAINRATEIN: ( 274 | "Rain Rate", 275 | f"{UnitOfLength.INCHES}/{UnitOfTime.HOURS}", 276 | TYPE_SENSOR, 277 | SensorDeviceClass.PRECIPITATION_INTENSITY, 278 | "mdi:water", 279 | S_IMPERIAL, 280 | SensorStateClass.MEASUREMENT, 281 | ), 282 | TYPE_EVENTRAININ: ( 283 | "Event Rain Rate", 284 | f"{UnitOfLength.INCHES}/{UnitOfTime.HOURS}", 285 | TYPE_SENSOR, 286 | SensorDeviceClass.PRECIPITATION_INTENSITY, 287 | "mdi:water", 288 | S_IMPERIAL, 289 | SensorStateClass.MEASUREMENT, 290 | ), 291 | TYPE_HOURLYRAININ: ( 292 | "Hourly Rain Rate", 293 | f"{UnitOfLength.INCHES}/{UnitOfTime.HOURS}", 294 | TYPE_SENSOR, 295 | SensorDeviceClass.PRECIPITATION_INTENSITY, 296 | "mdi:water", 297 | S_IMPERIAL, 298 | SensorStateClass.MEASUREMENT, 299 | ), 300 | TYPE_TOTALRAININ: ( 301 | "Total Rain Rate", 302 | f"{UnitOfLength.INCHES}", 303 | TYPE_SENSOR, 304 | SensorDeviceClass.PRECIPITATION, 305 | "mdi:water", 306 | S_IMPERIAL, 307 | SensorStateClass.TOTAL_INCREASING, 308 | ), 309 | TYPE_DAILYRAININ: ( 310 | "Daily Rain Rate", 311 | f"{UnitOfLength.INCHES}/{UnitOfTime.DAYS}", 312 | TYPE_SENSOR, 313 | SensorDeviceClass.PRECIPITATION_INTENSITY, 314 | "mdi:water", 315 | S_IMPERIAL, 316 | SensorStateClass.MEASUREMENT, 317 | ), 318 | TYPE_WEEKLYRAININ: ( 319 | "Weekly Rain Rate", 320 | f"{UnitOfLength.INCHES}/{UnitOfTime.WEEKS}", 321 | TYPE_SENSOR, 322 | None, # SensorDeviceClass.PRECIPITATION_INTENSITY, 323 | "mdi:water", 324 | S_IMPERIAL, 325 | SensorStateClass.MEASUREMENT, 326 | ), 327 | TYPE_MONTHLYRAININ: ( 328 | "Monthly Rain Rate", 329 | f"{UnitOfLength.INCHES}/{UnitOfTime.MONTHS}", 330 | TYPE_SENSOR, 331 | None, # SensorDeviceClass.PRECIPITATION_INTENSITY, 332 | "mdi:water", 333 | S_IMPERIAL, 334 | SensorStateClass.MEASUREMENT, 335 | ), 336 | TYPE_YEARLYRAININ: ( 337 | "Yearly Rain Rate", 338 | f"{UnitOfLength.INCHES}/{UnitOfTime.YEARS}", 339 | TYPE_SENSOR, 340 | None, # SensorDeviceClass.PRECIPITATION_INTENSITY, 341 | "mdi:water", 342 | S_IMPERIAL, 343 | SensorStateClass.MEASUREMENT, 344 | ), 345 | TYPE_RAINRATEMM: ( 346 | "Rain Rate", 347 | f"{UnitOfLength.MILLIMETERS}/{UnitOfTime.HOURS}", 348 | TYPE_SENSOR, 349 | SensorDeviceClass.PRECIPITATION_INTENSITY, 350 | "mdi:water", 351 | S_METRIC, 352 | SensorStateClass.MEASUREMENT, 353 | ), 354 | TYPE_EVENTRAINMM: ( 355 | "Event Rain Rate", 356 | f"{UnitOfLength.MILLIMETERS}/{UnitOfTime.HOURS}", 357 | TYPE_SENSOR, 358 | SensorDeviceClass.PRECIPITATION_INTENSITY, 359 | "mdi:water", 360 | S_METRIC, 361 | SensorStateClass.MEASUREMENT, 362 | ), 363 | TYPE_HOURLYRAINMM: ( 364 | "Hourly Rain Rate", 365 | f"{UnitOfLength.MILLIMETERS}/{UnitOfTime.HOURS}", 366 | TYPE_SENSOR, 367 | SensorDeviceClass.PRECIPITATION_INTENSITY, 368 | "mdi:water", 369 | S_METRIC, 370 | SensorStateClass.MEASUREMENT, 371 | ), 372 | TYPE_TOTALRAINMM: ( 373 | "Total Rain Rate", 374 | UnitOfLength.MILLIMETERS, 375 | TYPE_SENSOR, 376 | SensorDeviceClass.PRECIPITATION, 377 | "mdi:water", 378 | S_METRIC, 379 | SensorStateClass.TOTAL_INCREASING, 380 | ), 381 | TYPE_DAILYRAINMM: ( 382 | "Daily Rain Rate", 383 | f"{UnitOfLength.MILLIMETERS}/{UnitOfTime.DAYS}", 384 | TYPE_SENSOR, 385 | SensorDeviceClass.PRECIPITATION_INTENSITY, 386 | "mdi:water", 387 | S_METRIC, 388 | SensorStateClass.MEASUREMENT, 389 | ), 390 | TYPE_WEEKLYRAINMM: ( 391 | "Weekly Rain Rate", 392 | f"{UnitOfLength.MILLIMETERS}/{UnitOfTime.WEEKS}", 393 | TYPE_SENSOR, 394 | None, # SensorDeviceClass.PRECIPITATION_INTENSITY, 395 | "mdi:water", 396 | S_METRIC, 397 | SensorStateClass.MEASUREMENT, 398 | ), 399 | TYPE_MONTHLYRAINMM: ( 400 | "Monthly Rain Rate", 401 | f"{UnitOfLength.MILLIMETERS}/{UnitOfTime.MONTHS}", 402 | TYPE_SENSOR, 403 | None, # SensorDeviceClass.PRECIPITATION_INTENSITY, 404 | "mdi:water", 405 | S_METRIC, 406 | SensorStateClass.MEASUREMENT, 407 | ), 408 | TYPE_YEARLYRAINMM: ( 409 | "Yearly Rain Rate", 410 | f"{UnitOfLength.MILLIMETERS}/{UnitOfTime.YEARS}", 411 | TYPE_SENSOR, 412 | None, # SensorDeviceClass.PRECIPITATION_INTENSITY, 413 | "mdi:water", 414 | S_METRIC, 415 | SensorStateClass.MEASUREMENT, 416 | ), 417 | TYPE_HUMIDITY: ( 418 | "Humidity", 419 | PERCENTAGE, 420 | TYPE_SENSOR, 421 | SensorDeviceClass.HUMIDITY, 422 | "mdi:water-percent", 423 | 0, 424 | SensorStateClass.MEASUREMENT, 425 | ), 426 | TYPE_HUMIDITYIN: ( 427 | "Indoor Humidity", 428 | PERCENTAGE, 429 | TYPE_SENSOR, 430 | SensorDeviceClass.HUMIDITY, 431 | "mdi:water-percent", 432 | 0, 433 | SensorStateClass.MEASUREMENT, 434 | ), 435 | TYPE_HUMIDITY1: ( 436 | "Humidity 1", 437 | PERCENTAGE, 438 | TYPE_SENSOR, 439 | SensorDeviceClass.HUMIDITY, 440 | "mdi:water-percent", 441 | 0, 442 | SensorStateClass.MEASUREMENT, 443 | ), 444 | TYPE_HUMIDITY2: ( 445 | "Humidity 2", 446 | PERCENTAGE, 447 | TYPE_SENSOR, 448 | SensorDeviceClass.HUMIDITY, 449 | "mdi:water-percent", 450 | 0, 451 | SensorStateClass.MEASUREMENT, 452 | ), 453 | TYPE_HUMIDITY3: ( 454 | "Humidity 3", 455 | PERCENTAGE, 456 | TYPE_SENSOR, 457 | SensorDeviceClass.HUMIDITY, 458 | "mdi:water-percent", 459 | 0, 460 | SensorStateClass.MEASUREMENT, 461 | ), 462 | TYPE_HUMIDITY4: ( 463 | "Humidity 4", 464 | PERCENTAGE, 465 | TYPE_SENSOR, 466 | SensorDeviceClass.HUMIDITY, 467 | "mdi:water-percent", 468 | 0, 469 | SensorStateClass.MEASUREMENT, 470 | ), 471 | TYPE_HUMIDITY5: ( 472 | "Humidity 5", 473 | PERCENTAGE, 474 | TYPE_SENSOR, 475 | SensorDeviceClass.HUMIDITY, 476 | "mdi:water-percent", 477 | 0, 478 | SensorStateClass.MEASUREMENT, 479 | ), 480 | TYPE_HUMIDITY6: ( 481 | "Humidity 6", 482 | PERCENTAGE, 483 | TYPE_SENSOR, 484 | SensorDeviceClass.HUMIDITY, 485 | "mdi:water-percent", 486 | 0, 487 | SensorStateClass.MEASUREMENT, 488 | ), 489 | TYPE_HUMIDITY7: ( 490 | "Humidity 7", 491 | PERCENTAGE, 492 | TYPE_SENSOR, 493 | SensorDeviceClass.HUMIDITY, 494 | "mdi:water-percent", 495 | 0, 496 | SensorStateClass.MEASUREMENT, 497 | ), 498 | TYPE_HUMIDITY8: ( 499 | "Humidity 8", 500 | PERCENTAGE, 501 | TYPE_SENSOR, 502 | SensorDeviceClass.HUMIDITY, 503 | "mdi:water-percent", 504 | 0, 505 | SensorStateClass.MEASUREMENT, 506 | ), 507 | TYPE_WINDDIR: ( 508 | "Wind Direction", 509 | DEGREE, 510 | TYPE_SENSOR, 511 | None, 512 | "mdi:compass", 513 | 0, 514 | SensorStateClass.MEASUREMENT, 515 | ), 516 | TYPE_WINDDIR_A10: ( 517 | "Wind Direction 10m Avg", 518 | DEGREE, 519 | TYPE_SENSOR, 520 | None, 521 | "mdi:compass", 522 | 0, 523 | SensorStateClass.MEASUREMENT, 524 | ), 525 | TYPE_WINDSPEEDKMH: ( 526 | "Wind Speed", 527 | UnitOfSpeed.KILOMETERS_PER_HOUR, 528 | TYPE_SENSOR, 529 | SensorDeviceClass.WIND_SPEED, 530 | "mdi:weather-windy", 531 | S_METRIC, 532 | SensorStateClass.MEASUREMENT, 533 | ), 534 | TYPE_WINDSPEEDKMH_A10: ( 535 | "Wind Speed 10m Avg", 536 | UnitOfSpeed.KILOMETERS_PER_HOUR, 537 | TYPE_SENSOR, 538 | SensorDeviceClass.WIND_SPEED, 539 | "mdi:weather-windy", 540 | S_METRIC, 541 | SensorStateClass.MEASUREMENT, 542 | ), 543 | TYPE_WINDGUSTKMH: ( 544 | "Wind Gust", 545 | UnitOfSpeed.KILOMETERS_PER_HOUR, 546 | TYPE_SENSOR, 547 | SensorDeviceClass.WIND_SPEED, 548 | "mdi:weather-windy", 549 | S_METRIC, 550 | SensorStateClass.MEASUREMENT, 551 | ), 552 | TYPE_WINDSPEEDMPH: ( 553 | "Wind Speed", 554 | UnitOfSpeed.MILES_PER_HOUR, 555 | TYPE_SENSOR, 556 | SensorDeviceClass.WIND_SPEED, 557 | "mdi:weather-windy", 558 | S_IMPERIAL, 559 | SensorStateClass.MEASUREMENT, 560 | ), 561 | TYPE_WINDSPEEDMPH_A10: ( 562 | "Wind Speed 10m Avg", 563 | UnitOfSpeed.MILES_PER_HOUR, 564 | TYPE_SENSOR, 565 | SensorDeviceClass.WIND_SPEED, 566 | "mdi:weather-windy", 567 | S_IMPERIAL, 568 | SensorStateClass.MEASUREMENT, 569 | ), 570 | TYPE_WINDGUSTMPH: ( 571 | "Wind Gust", 572 | UnitOfSpeed.MILES_PER_HOUR, 573 | TYPE_SENSOR, 574 | SensorDeviceClass.WIND_SPEED, 575 | "mdi:weather-windy", 576 | S_IMPERIAL, 577 | SensorStateClass.MEASUREMENT, 578 | ), 579 | TYPE_MAXDAILYGUST: ( 580 | "Max Daily Wind Gust", 581 | UnitOfSpeed.MILES_PER_HOUR, 582 | TYPE_SENSOR, 583 | SensorDeviceClass.WIND_SPEED, 584 | "mdi:weather-windy", 585 | S_IMPERIAL, 586 | SensorStateClass.MEASUREMENT, 587 | ), 588 | TYPE_MAXDAILYGUSTKMH: ( 589 | "Max Daily Wind Gust", 590 | UnitOfSpeed.KILOMETERS_PER_HOUR, 591 | TYPE_SENSOR, 592 | SensorDeviceClass.WIND_SPEED, 593 | "mdi:weather-windy", 594 | S_METRIC, 595 | SensorStateClass.MEASUREMENT, 596 | ), 597 | TYPE_WINDGUSTMS: ( 598 | "Wind Gust", 599 | UnitOfSpeed.METERS_PER_SECOND, 600 | TYPE_SENSOR, 601 | SensorDeviceClass.WIND_SPEED, 602 | "mdi:weather-windy", 603 | S_METRIC_MS, 604 | SensorStateClass.MEASUREMENT, 605 | ), 606 | TYPE_WINDSPEEDMS: ( 607 | "Wind Speed", 608 | UnitOfSpeed.METERS_PER_SECOND, 609 | TYPE_SENSOR, 610 | SensorDeviceClass.WIND_SPEED, 611 | "mdi:weather-windy", 612 | S_METRIC_MS, 613 | SensorStateClass.MEASUREMENT, 614 | ), 615 | TYPE_WINDSPEEDMS_A10: ( 616 | "Wind Speed", 617 | UnitOfSpeed.METERS_PER_SECOND, 618 | TYPE_SENSOR, 619 | SensorDeviceClass.WIND_SPEED, 620 | "mdi:weather-windy", 621 | S_METRIC_MS, 622 | SensorStateClass.MEASUREMENT, 623 | ), 624 | TYPE_MAXDAILYGUSTMS: ( 625 | "Max Daily Wind Gust", 626 | UnitOfSpeed.METERS_PER_SECOND, 627 | TYPE_SENSOR, 628 | SensorDeviceClass.WIND_SPEED, 629 | "mdi:weather-windy", 630 | S_METRIC_MS, 631 | SensorStateClass.MEASUREMENT, 632 | ), 633 | TYPE_TEMPC: ( 634 | "Outdoor Temperature", 635 | UnitOfTemperature.CELSIUS, 636 | TYPE_SENSOR, 637 | SensorDeviceClass.TEMPERATURE, 638 | "mdi:thermometer", 639 | 0, 640 | SensorStateClass.MEASUREMENT, 641 | ), 642 | TYPE_TEMP1C: ( 643 | "Temperature 1", 644 | UnitOfTemperature.CELSIUS, 645 | TYPE_SENSOR, 646 | SensorDeviceClass.TEMPERATURE, 647 | "mdi:thermometer", 648 | 0, 649 | SensorStateClass.MEASUREMENT, 650 | ), 651 | TYPE_TEMP2C: ( 652 | "Temperature 2", 653 | UnitOfTemperature.CELSIUS, 654 | TYPE_SENSOR, 655 | SensorDeviceClass.TEMPERATURE, 656 | "mdi:thermometer", 657 | 0, 658 | SensorStateClass.MEASUREMENT, 659 | ), 660 | TYPE_TEMP3C: ( 661 | "Temperature 3", 662 | UnitOfTemperature.CELSIUS, 663 | TYPE_SENSOR, 664 | SensorDeviceClass.TEMPERATURE, 665 | "mdi:thermometer", 666 | 0, 667 | SensorStateClass.MEASUREMENT, 668 | ), 669 | TYPE_TEMP4C: ( 670 | "Temperature 4", 671 | UnitOfTemperature.CELSIUS, 672 | TYPE_SENSOR, 673 | SensorDeviceClass.TEMPERATURE, 674 | "mdi:thermometer", 675 | 0, 676 | SensorStateClass.MEASUREMENT, 677 | ), 678 | TYPE_TEMP5C: ( 679 | "Temperature 5", 680 | UnitOfTemperature.CELSIUS, 681 | TYPE_SENSOR, 682 | SensorDeviceClass.TEMPERATURE, 683 | "mdi:thermometer", 684 | 0, 685 | SensorStateClass.MEASUREMENT, 686 | ), 687 | TYPE_TEMP6C: ( 688 | "Temperature 6", 689 | UnitOfTemperature.CELSIUS, 690 | TYPE_SENSOR, 691 | SensorDeviceClass.TEMPERATURE, 692 | "mdi:thermometer", 693 | 0, 694 | SensorStateClass.MEASUREMENT, 695 | ), 696 | TYPE_TEMP7C: ( 697 | "Temperature 7", 698 | UnitOfTemperature.CELSIUS, 699 | TYPE_SENSOR, 700 | SensorDeviceClass.TEMPERATURE, 701 | "mdi:thermometer", 702 | 0, 703 | SensorStateClass.MEASUREMENT, 704 | ), 705 | TYPE_TEMP8C: ( 706 | "Temperature 8", 707 | UnitOfTemperature.CELSIUS, 708 | TYPE_SENSOR, 709 | SensorDeviceClass.TEMPERATURE, 710 | "mdi:thermometer", 711 | 0, 712 | SensorStateClass.MEASUREMENT, 713 | ), 714 | TYPE_TEMPINC: ( 715 | "Indoor Temperature", 716 | UnitOfTemperature.CELSIUS, 717 | TYPE_SENSOR, 718 | SensorDeviceClass.TEMPERATURE, 719 | "mdi:thermometer", 720 | 0, 721 | SensorStateClass.MEASUREMENT, 722 | ), 723 | TYPE_DEWPOINTC: ( 724 | "Dewpoint", 725 | UnitOfTemperature.CELSIUS, 726 | TYPE_SENSOR, 727 | SensorDeviceClass.TEMPERATURE, 728 | "mdi:thermometer", 729 | 0, 730 | SensorStateClass.MEASUREMENT, 731 | ), 732 | TYPE_DEWPOINTINC: ( 733 | "Indoor Dewpoint", 734 | UnitOfTemperature.CELSIUS, 735 | TYPE_SENSOR, 736 | SensorDeviceClass.TEMPERATURE, 737 | "mdi:thermometer", 738 | 0, 739 | SensorStateClass.MEASUREMENT, 740 | ), 741 | TYPE_DEWPOINT1C: ( 742 | "Dewpoint 1", 743 | UnitOfTemperature.CELSIUS, 744 | TYPE_SENSOR, 745 | SensorDeviceClass.TEMPERATURE, 746 | "mdi:thermometer", 747 | 0, 748 | SensorStateClass.MEASUREMENT, 749 | ), 750 | TYPE_DEWPOINT2C: ( 751 | "Dewpoint 2", 752 | UnitOfTemperature.CELSIUS, 753 | TYPE_SENSOR, 754 | SensorDeviceClass.TEMPERATURE, 755 | "mdi:thermometer", 756 | 0, 757 | SensorStateClass.MEASUREMENT, 758 | ), 759 | TYPE_DEWPOINT3C: ( 760 | "Dewpoint 3", 761 | UnitOfTemperature.CELSIUS, 762 | TYPE_SENSOR, 763 | SensorDeviceClass.TEMPERATURE, 764 | "mdi:thermometer", 765 | 0, 766 | SensorStateClass.MEASUREMENT, 767 | ), 768 | TYPE_DEWPOINT4C: ( 769 | "Dewpoint 4", 770 | UnitOfTemperature.CELSIUS, 771 | TYPE_SENSOR, 772 | SensorDeviceClass.TEMPERATURE, 773 | "mdi:thermometer", 774 | 0, 775 | SensorStateClass.MEASUREMENT, 776 | ), 777 | TYPE_DEWPOINT5C: ( 778 | "Dewpoint 5", 779 | UnitOfTemperature.CELSIUS, 780 | TYPE_SENSOR, 781 | SensorDeviceClass.TEMPERATURE, 782 | "mdi:thermometer", 783 | 0, 784 | SensorStateClass.MEASUREMENT, 785 | ), 786 | TYPE_DEWPOINT6C: ( 787 | "Dewpoint 6", 788 | UnitOfTemperature.CELSIUS, 789 | TYPE_SENSOR, 790 | SensorDeviceClass.TEMPERATURE, 791 | "mdi:thermometer", 792 | 0, 793 | SensorStateClass.MEASUREMENT, 794 | ), 795 | TYPE_DEWPOINT7C: ( 796 | "Dewpoint 7", 797 | UnitOfTemperature.CELSIUS, 798 | TYPE_SENSOR, 799 | SensorDeviceClass.TEMPERATURE, 800 | "mdi:thermometer", 801 | 0, 802 | SensorStateClass.MEASUREMENT, 803 | ), 804 | TYPE_DEWPOINT8C: ( 805 | "Dewpoint 8", 806 | UnitOfTemperature.CELSIUS, 807 | TYPE_SENSOR, 808 | SensorDeviceClass.TEMPERATURE, 809 | "mdi:thermometer", 810 | 0, 811 | SensorStateClass.MEASUREMENT, 812 | ), 813 | TYPE_WINDCHILLC: ( 814 | "Windchill", 815 | UnitOfTemperature.CELSIUS, 816 | TYPE_SENSOR, 817 | SensorDeviceClass.TEMPERATURE, 818 | "mdi:thermometer", 819 | 0, 820 | SensorStateClass.MEASUREMENT, 821 | ), 822 | TYPE_SOLARRADIATION: ( 823 | "Solar Radiation", 824 | f"{UnitOfPower.WATT}/m²", 825 | TYPE_SENSOR, 826 | SensorDeviceClass.IRRADIANCE, 827 | "mdi:weather-sunny", 828 | 0, 829 | SensorStateClass.MEASUREMENT, 830 | ), 831 | TYPE_UV: ( 832 | "UV Index", 833 | UV_INDEX, 834 | TYPE_SENSOR, 835 | None, 836 | "mdi:sunglasses", 837 | 0, 838 | SensorStateClass.MEASUREMENT, 839 | ), 840 | TYPE_SOILMOISTURE1: ( 841 | "Soil Moisture 1", 842 | PERCENTAGE, 843 | TYPE_SENSOR, 844 | SensorDeviceClass.HUMIDITY, 845 | "mdi:water-percent", 846 | 0, 847 | SensorStateClass.MEASUREMENT, 848 | ), 849 | TYPE_SOILMOISTURE2: ( 850 | "Soil Moisture 2", 851 | PERCENTAGE, 852 | TYPE_SENSOR, 853 | SensorDeviceClass.HUMIDITY, 854 | "mdi:water-percent", 855 | 0, 856 | SensorStateClass.MEASUREMENT, 857 | ), 858 | TYPE_SOILMOISTURE3: ( 859 | "Soil Moisture 3", 860 | PERCENTAGE, 861 | TYPE_SENSOR, 862 | SensorDeviceClass.HUMIDITY, 863 | "mdi:water-percent", 864 | 0, 865 | SensorStateClass.MEASUREMENT, 866 | ), 867 | TYPE_SOILMOISTURE4: ( 868 | "Soil Moisture 4", 869 | PERCENTAGE, 870 | TYPE_SENSOR, 871 | SensorDeviceClass.HUMIDITY, 872 | "mdi:water-percent", 873 | 0, 874 | SensorStateClass.MEASUREMENT, 875 | ), 876 | TYPE_SOILMOISTURE5: ( 877 | "Soil Moisture 5", 878 | PERCENTAGE, 879 | TYPE_SENSOR, 880 | SensorDeviceClass.HUMIDITY, 881 | "mdi:water-percent", 882 | 0, 883 | SensorStateClass.MEASUREMENT, 884 | ), 885 | TYPE_SOILMOISTURE6: ( 886 | "Soil Moisture 6", 887 | PERCENTAGE, 888 | TYPE_SENSOR, 889 | SensorDeviceClass.HUMIDITY, 890 | "mdi:water-percent", 891 | 0, 892 | SensorStateClass.MEASUREMENT, 893 | ), 894 | TYPE_SOILMOISTURE7: ( 895 | "Soil Moisture 7", 896 | PERCENTAGE, 897 | TYPE_SENSOR, 898 | SensorDeviceClass.HUMIDITY, 899 | "mdi:water-percent", 900 | 0, 901 | SensorStateClass.MEASUREMENT, 902 | ), 903 | TYPE_SOILMOISTURE8: ( 904 | "Soil Moisture 8", 905 | PERCENTAGE, 906 | TYPE_SENSOR, 907 | SensorDeviceClass.HUMIDITY, 908 | "mdi:water-percent", 909 | 0, 910 | SensorStateClass.MEASUREMENT, 911 | ), 912 | TYPE_PM25_CH1: ( 913 | "PM2.5 1", 914 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 915 | TYPE_SENSOR, 916 | SensorDeviceClass.PM25, 917 | "mdi:eye", 918 | 0, 919 | SensorStateClass.MEASUREMENT, 920 | ), 921 | TYPE_PM25_CH2: ( 922 | "PM2.5 2", 923 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 924 | TYPE_SENSOR, 925 | SensorDeviceClass.PM25, 926 | "mdi:eye", 927 | 0, 928 | SensorStateClass.MEASUREMENT, 929 | ), 930 | TYPE_PM25_CH3: ( 931 | "PM2.5 3", 932 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 933 | TYPE_SENSOR, 934 | SensorDeviceClass.PM25, 935 | "mdi:eye", 936 | 0, 937 | SensorStateClass.MEASUREMENT, 938 | ), 939 | TYPE_PM25_CH4: ( 940 | "PM2.5 4", 941 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 942 | TYPE_SENSOR, 943 | SensorDeviceClass.PM25, 944 | "mdi:eye", 945 | 0, 946 | SensorStateClass.MEASUREMENT, 947 | ), 948 | TYPE_PM25_AVG_24H_CH1: ( 949 | "PM2.5 24h average 1", 950 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 951 | TYPE_SENSOR, 952 | SensorDeviceClass.PM25, 953 | "mdi:eye", 954 | 0, 955 | SensorStateClass.MEASUREMENT, 956 | ), 957 | TYPE_PM25_AVG_24H_CH2: ( 958 | "PM2.5 24h average 2", 959 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 960 | TYPE_SENSOR, 961 | SensorDeviceClass.PM25, 962 | "mdi:eye", 963 | 0, 964 | SensorStateClass.MEASUREMENT, 965 | ), 966 | TYPE_PM25_AVG_24H_CH3: ( 967 | "PM2.5 24h average 3", 968 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 969 | TYPE_SENSOR, 970 | SensorDeviceClass.PM25, 971 | "mdi:eye", 972 | 0, 973 | SensorStateClass.MEASUREMENT, 974 | ), 975 | TYPE_PM25_AVG_24H_CH4: ( 976 | "PM2.5 24h average 4", 977 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 978 | TYPE_SENSOR, 979 | SensorDeviceClass.PM25, 980 | "mdi:eye", 981 | 0, 982 | SensorStateClass.MEASUREMENT, 983 | ), 984 | TYPE_LIGHTNING_TIME: ( 985 | "Last Lightning strike", 986 | "", 987 | TYPE_SENSOR, 988 | SensorDeviceClass.TIMESTAMP, 989 | "mdi:clock", 990 | 0, 991 | SensorStateClass.MEASUREMENT, 992 | ), 993 | TYPE_LIGHTNING_NUM: ( 994 | "Lightning strikes", 995 | f"strikes/{UnitOfTime.DAYS}", 996 | TYPE_SENSOR, 997 | None, 998 | "mdi:weather-lightning", 999 | 0, 1000 | SensorStateClass.TOTAL_INCREASING, 1001 | ), 1002 | TYPE_LIGHTNING_KM: ( 1003 | "Lightning strike distance", 1004 | UnitOfLength.KILOMETERS, 1005 | TYPE_SENSOR, 1006 | None, 1007 | "mdi:ruler", 1008 | S_METRIC, 1009 | SensorStateClass.MEASUREMENT, 1010 | ), 1011 | TYPE_LIGHTNING_MI: ( 1012 | "Lightning strike distance", 1013 | UnitOfLength.MILES, 1014 | TYPE_SENSOR, 1015 | None, 1016 | "mdi:ruler", 1017 | S_IMPERIAL, 1018 | SensorStateClass.MEASUREMENT, 1019 | ), 1020 | TYPE_LEAK_CH1: ( 1021 | "Leak Detection 1", 1022 | LEAK_DETECTED, 1023 | TYPE_BINARY_SENSOR, 1024 | SensorDeviceClass.MOISTURE, 1025 | "mdi:leak", 1026 | 0, 1027 | None, 1028 | ), 1029 | TYPE_LEAK_CH2: ( 1030 | "Leak Detection 2", 1031 | LEAK_DETECTED, 1032 | TYPE_BINARY_SENSOR, 1033 | SensorDeviceClass.MOISTURE, 1034 | "mdi:leak", 1035 | 0, 1036 | None, 1037 | ), 1038 | TYPE_LEAK_CH3: ( 1039 | "Leak Detection 3", 1040 | LEAK_DETECTED, 1041 | TYPE_BINARY_SENSOR, 1042 | SensorDeviceClass.MOISTURE, 1043 | "mdi:leak", 1044 | 0, 1045 | None, 1046 | ), 1047 | TYPE_LEAK_CH4: ( 1048 | "Leak Detection 4", 1049 | LEAK_DETECTED, 1050 | TYPE_BINARY_SENSOR, 1051 | SensorDeviceClass.MOISTURE, 1052 | "mdi:leak", 1053 | 0, 1054 | None, 1055 | ), 1056 | TYPE_CO2_PM25: ( 1057 | "WH45 PM2.5", 1058 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 1059 | TYPE_SENSOR, 1060 | SensorDeviceClass.PM25, 1061 | "mdi:eye", 1062 | 0, 1063 | SensorStateClass.MEASUREMENT, 1064 | ), 1065 | TYPE_CO2_PM25_AVG_24H: ( 1066 | "WH45 PM2.5 24h average", 1067 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 1068 | TYPE_SENSOR, 1069 | SensorDeviceClass.PM25, 1070 | "mdi:eye", 1071 | 0, 1072 | SensorStateClass.MEASUREMENT, 1073 | ), 1074 | TYPE_CO2_PM10: ( 1075 | "WH45 PM10", 1076 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 1077 | TYPE_SENSOR, 1078 | SensorDeviceClass.PM10, 1079 | "mdi:eye", 1080 | 0, 1081 | SensorStateClass.MEASUREMENT, 1082 | ), 1083 | TYPE_CO2_PM10_AVG_24H: ( 1084 | "WH45 PM10 24h average", 1085 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 1086 | TYPE_SENSOR, 1087 | SensorDeviceClass.PM10, 1088 | "mdi:eye", 1089 | 0, 1090 | SensorStateClass.MEASUREMENT, 1091 | ), 1092 | TYPE_CO2_HUMIDITY: ( 1093 | "WH45 Humidity", 1094 | PERCENTAGE, 1095 | TYPE_SENSOR, 1096 | SensorDeviceClass.HUMIDITY, 1097 | "mdi:water-percent", 1098 | 0, 1099 | SensorStateClass.MEASUREMENT, 1100 | ), 1101 | TYPE_CO2_TEMPC: ( 1102 | "WH45 Temperature", 1103 | UnitOfTemperature.CELSIUS, 1104 | TYPE_SENSOR, 1105 | SensorDeviceClass.TEMPERATURE, 1106 | "mdi:thermometer", 1107 | 0, 1108 | SensorStateClass.MEASUREMENT, 1109 | ), 1110 | TYPE_CO2_CO2: ( 1111 | "WH45 CO2", 1112 | CONCENTRATION_PARTS_PER_MILLION, 1113 | TYPE_SENSOR, 1114 | None, # SensorDeviceClass.CARBON_DIOXIDE, 1115 | "mdi:molecule-co2", 1116 | 0, 1117 | SensorStateClass.MEASUREMENT, 1118 | ), 1119 | TYPE_CO2_CO2_AVG_24H: ( 1120 | "WH45 CO2 24h average", 1121 | CONCENTRATION_PARTS_PER_MILLION, 1122 | TYPE_SENSOR, 1123 | None, # SensorDeviceClass.CARBON_DIOXIDE, 1124 | "mdi:molecule-co2", 1125 | 0, 1126 | SensorStateClass.MEASUREMENT, 1127 | ), 1128 | TYPE_CO2_BATT: ( 1129 | "WH45 Battery", 1130 | PERCENTAGE, 1131 | TYPE_SENSOR, 1132 | SensorDeviceClass.BATTERY, 1133 | "mdi:battery", 1134 | 0, 1135 | SensorStateClass.MEASUREMENT, 1136 | ), 1137 | TYPE_WH25BATT: ( 1138 | "WH25 Battery", 1139 | "BATT", 1140 | TYPE_BINARY_SENSOR, 1141 | SensorDeviceClass.BATTERY, 1142 | "mdi:battery", 1143 | 0, 1144 | None, 1145 | ), 1146 | TYPE_WH26BATT: ( 1147 | "WH26 Battery", 1148 | "BATT", 1149 | TYPE_BINARY_SENSOR, 1150 | SensorDeviceClass.BATTERY, 1151 | "mdi:battery", 1152 | 0, 1153 | None, 1154 | ), 1155 | TYPE_WH40BATT: ( 1156 | "WH40 Battery", 1157 | UnitOfElectricPotential.VOLT, 1158 | TYPE_SENSOR, 1159 | SensorDeviceClass.VOLTAGE, 1160 | "mdi:battery", 1161 | 0, 1162 | SensorStateClass.MEASUREMENT, 1163 | ), 1164 | TYPE_WH57BATT: ( 1165 | "WH57 Battery", 1166 | PERCENTAGE, 1167 | TYPE_SENSOR, 1168 | SensorDeviceClass.BATTERY, 1169 | "mdi:battery", 1170 | 0, 1171 | SensorStateClass.MEASUREMENT, 1172 | ), 1173 | TYPE_WH65BATT: ( 1174 | "WH65 Battery", 1175 | "BATT", 1176 | TYPE_BINARY_SENSOR, 1177 | SensorDeviceClass.BATTERY, 1178 | "mdi:battery", 1179 | 0, 1180 | None, 1181 | ), 1182 | TYPE_WH68BATT: ( 1183 | "WH68 Battery", 1184 | UnitOfElectricPotential.VOLT, 1185 | TYPE_SENSOR, 1186 | SensorDeviceClass.BATTERY, 1187 | "mdi:battery", 1188 | 0, 1189 | SensorStateClass.MEASUREMENT, 1190 | ), 1191 | TYPE_WH80BATT: ( 1192 | "WH80 Battery", 1193 | UnitOfElectricPotential.VOLT, 1194 | TYPE_SENSOR, 1195 | SensorDeviceClass.BATTERY, 1196 | "mdi:battery", 1197 | 0, 1198 | SensorStateClass.MEASUREMENT, 1199 | ), 1200 | TYPE_SOILBATT1: ( 1201 | "Soil Moisture 1 Battery", 1202 | UnitOfElectricPotential.VOLT, 1203 | TYPE_SENSOR, 1204 | SensorDeviceClass.VOLTAGE, 1205 | "mdi:battery", 1206 | 0, 1207 | SensorStateClass.MEASUREMENT, 1208 | ), 1209 | TYPE_SOILBATT2: ( 1210 | "Soil Moisture 2 Battery", 1211 | UnitOfElectricPotential.VOLT, 1212 | TYPE_SENSOR, 1213 | SensorDeviceClass.VOLTAGE, 1214 | "mdi:battery", 1215 | 0, 1216 | SensorStateClass.MEASUREMENT, 1217 | ), 1218 | TYPE_SOILBATT3: ( 1219 | "Soil Moisture 3 Battery", 1220 | UnitOfElectricPotential.VOLT, 1221 | TYPE_SENSOR, 1222 | SensorDeviceClass.VOLTAGE, 1223 | "mdi:battery", 1224 | 0, 1225 | SensorStateClass.MEASUREMENT, 1226 | ), 1227 | TYPE_SOILBATT4: ( 1228 | "Soil Moisture 4 Battery", 1229 | UnitOfElectricPotential.VOLT, 1230 | TYPE_SENSOR, 1231 | SensorDeviceClass.VOLTAGE, 1232 | "mdi:battery", 1233 | 0, 1234 | SensorStateClass.MEASUREMENT, 1235 | ), 1236 | TYPE_SOILBATT5: ( 1237 | "Soil Moisture 5 Battery", 1238 | UnitOfElectricPotential.VOLT, 1239 | TYPE_SENSOR, 1240 | SensorDeviceClass.VOLTAGE, 1241 | "mdi:battery", 1242 | 0, 1243 | SensorStateClass.MEASUREMENT, 1244 | ), 1245 | TYPE_SOILBATT6: ( 1246 | "Soil Moisture 6 Battery", 1247 | UnitOfElectricPotential.VOLT, 1248 | TYPE_SENSOR, 1249 | SensorDeviceClass.VOLTAGE, 1250 | "mdi:battery", 1251 | 0, 1252 | SensorStateClass.MEASUREMENT, 1253 | ), 1254 | TYPE_SOILBATT7: ( 1255 | "Soil Moisture 7 Battery", 1256 | UnitOfElectricPotential.VOLT, 1257 | TYPE_SENSOR, 1258 | SensorDeviceClass.VOLTAGE, 1259 | "mdi:battery", 1260 | 0, 1261 | SensorStateClass.MEASUREMENT, 1262 | ), 1263 | TYPE_SOILBATT8: ( 1264 | "Soil Moisture 8 Battery", 1265 | UnitOfElectricPotential.VOLT, 1266 | TYPE_SENSOR, 1267 | SensorDeviceClass.VOLTAGE, 1268 | "mdi:battery", 1269 | 0, 1270 | SensorStateClass.MEASUREMENT, 1271 | ), 1272 | TYPE_BATTERY1: ( 1273 | "Battery 1", 1274 | "BATT", 1275 | TYPE_BINARY_SENSOR, 1276 | SensorDeviceClass.BATTERY, 1277 | "mdi:battery", 1278 | 0, 1279 | None, 1280 | ), 1281 | TYPE_BATTERY2: ( 1282 | "Battery 2", 1283 | "BATT", 1284 | TYPE_BINARY_SENSOR, 1285 | SensorDeviceClass.BATTERY, 1286 | "mdi:battery", 1287 | 0, 1288 | None, 1289 | ), 1290 | TYPE_BATTERY3: ( 1291 | "Battery 3", 1292 | "BATT", 1293 | TYPE_BINARY_SENSOR, 1294 | SensorDeviceClass.BATTERY, 1295 | "mdi:battery", 1296 | 0, 1297 | None, 1298 | ), 1299 | TYPE_BATTERY4: ( 1300 | "Battery 4", 1301 | "BATT", 1302 | TYPE_BINARY_SENSOR, 1303 | SensorDeviceClass.BATTERY, 1304 | "mdi:battery", 1305 | 0, 1306 | None, 1307 | ), 1308 | TYPE_BATTERY5: ( 1309 | "Battery 5", 1310 | "BATT", 1311 | TYPE_BINARY_SENSOR, 1312 | SensorDeviceClass.BATTERY, 1313 | "mdi:battery", 1314 | 0, 1315 | None, 1316 | ), 1317 | TYPE_BATTERY6: ( 1318 | "Battery 6", 1319 | "BATT", 1320 | TYPE_BINARY_SENSOR, 1321 | SensorDeviceClass.BATTERY, 1322 | "mdi:battery", 1323 | 0, 1324 | None, 1325 | ), 1326 | TYPE_BATTERY7: ( 1327 | "Battery 7", 1328 | "BATT", 1329 | TYPE_BINARY_SENSOR, 1330 | SensorDeviceClass.BATTERY, 1331 | "mdi:battery", 1332 | 0, 1333 | None, 1334 | ), 1335 | TYPE_BATTERY8: ( 1336 | "Battery 8", 1337 | "BATT", 1338 | TYPE_BINARY_SENSOR, 1339 | SensorDeviceClass.BATTERY, 1340 | "mdi:battery", 1341 | 0, 1342 | None, 1343 | ), 1344 | TYPE_PM25BATT1: ( 1345 | "PM2.5 1 Battery", 1346 | PERCENTAGE, 1347 | TYPE_SENSOR, 1348 | SensorDeviceClass.BATTERY, 1349 | "mdi:battery", 1350 | 0, 1351 | SensorStateClass.MEASUREMENT, 1352 | ), 1353 | TYPE_PM25BATT2: ( 1354 | "PM2.5 2 Battery", 1355 | PERCENTAGE, 1356 | TYPE_SENSOR, 1357 | SensorDeviceClass.BATTERY, 1358 | "mdi:battery", 1359 | 0, 1360 | SensorStateClass.MEASUREMENT, 1361 | ), 1362 | TYPE_PM25BATT3: ( 1363 | "PM2.5 3 Battery", 1364 | PERCENTAGE, 1365 | TYPE_SENSOR, 1366 | SensorDeviceClass.BATTERY, 1367 | "mdi:battery", 1368 | 0, 1369 | SensorStateClass.MEASUREMENT, 1370 | ), 1371 | TYPE_PM25BATT4: ( 1372 | "PM2.5 4 Battery", 1373 | PERCENTAGE, 1374 | TYPE_SENSOR, 1375 | SensorDeviceClass.BATTERY, 1376 | "mdi:battery", 1377 | 0, 1378 | SensorStateClass.MEASUREMENT, 1379 | ), 1380 | TYPE_PM25BATT5: ( 1381 | "PM2.5 5 Battery", 1382 | PERCENTAGE, 1383 | TYPE_SENSOR, 1384 | SensorDeviceClass.BATTERY, 1385 | "mdi:battery", 1386 | 0, 1387 | SensorStateClass.MEASUREMENT, 1388 | ), 1389 | TYPE_PM25BATT6: ( 1390 | "PM2.5 6 Battery", 1391 | PERCENTAGE, 1392 | TYPE_SENSOR, 1393 | SensorDeviceClass.BATTERY, 1394 | "mdi:battery", 1395 | 0, 1396 | SensorStateClass.MEASUREMENT, 1397 | ), 1398 | TYPE_PM25BATT7: ( 1399 | "PM2.5 7 Battery", 1400 | PERCENTAGE, 1401 | TYPE_SENSOR, 1402 | SensorDeviceClass.BATTERY, 1403 | "mdi:battery", 1404 | 0, 1405 | SensorStateClass.MEASUREMENT, 1406 | ), 1407 | TYPE_PM25BATT8: ( 1408 | "PM2.5 8 Battery", 1409 | PERCENTAGE, 1410 | TYPE_SENSOR, 1411 | SensorDeviceClass.BATTERY, 1412 | "mdi:battery", 1413 | 0, 1414 | SensorStateClass.MEASUREMENT, 1415 | ), 1416 | TYPE_LEAKBATT1: ( 1417 | "Leak 1 Battery", 1418 | PERCENTAGE, 1419 | TYPE_SENSOR, 1420 | SensorDeviceClass.BATTERY, 1421 | "mdi:battery", 1422 | 0, 1423 | SensorStateClass.MEASUREMENT, 1424 | ), 1425 | TYPE_LEAKBATT2: ( 1426 | "Leak 2 Battery", 1427 | PERCENTAGE, 1428 | TYPE_SENSOR, 1429 | SensorDeviceClass.BATTERY, 1430 | "mdi:battery", 1431 | 0, 1432 | SensorStateClass.MEASUREMENT, 1433 | ), 1434 | TYPE_LEAKBATT3: ( 1435 | "Leak 3 Battery", 1436 | PERCENTAGE, 1437 | TYPE_SENSOR, 1438 | SensorDeviceClass.BATTERY, 1439 | "mdi:battery", 1440 | 0, 1441 | SensorStateClass.MEASUREMENT, 1442 | ), 1443 | TYPE_LEAKBATT4: ( 1444 | "Leak 4 Battery", 1445 | PERCENTAGE, 1446 | TYPE_SENSOR, 1447 | SensorDeviceClass.BATTERY, 1448 | "mdi:battery", 1449 | 0, 1450 | SensorStateClass.MEASUREMENT, 1451 | ), 1452 | TYPE_LEAKBATT5: ( 1453 | "Leak 5 Battery", 1454 | PERCENTAGE, 1455 | TYPE_SENSOR, 1456 | SensorDeviceClass.BATTERY, 1457 | "mdi:battery", 1458 | 0, 1459 | SensorStateClass.MEASUREMENT, 1460 | ), 1461 | TYPE_LEAKBATT6: ( 1462 | "Leak 6 Battery", 1463 | PERCENTAGE, 1464 | TYPE_SENSOR, 1465 | SensorDeviceClass.BATTERY, 1466 | "mdi:battery", 1467 | 0, 1468 | SensorStateClass.MEASUREMENT, 1469 | ), 1470 | TYPE_LEAKBATT7: ( 1471 | "Leak 7 Battery", 1472 | PERCENTAGE, 1473 | TYPE_SENSOR, 1474 | SensorDeviceClass.BATTERY, 1475 | "mdi:battery", 1476 | 0, 1477 | SensorStateClass.MEASUREMENT, 1478 | ), 1479 | TYPE_LEAKBATT8: ( 1480 | "Leak 8 Battery", 1481 | PERCENTAGE, 1482 | TYPE_SENSOR, 1483 | SensorDeviceClass.BATTERY, 1484 | "mdi:battery", 1485 | 0, 1486 | SensorStateClass.MEASUREMENT, 1487 | ), 1488 | TYPE_WN34TEMP1C: ( 1489 | "Soil Temperature 1", 1490 | UnitOfTemperature.CELSIUS, 1491 | TYPE_SENSOR, 1492 | SensorDeviceClass.TEMPERATURE, 1493 | "mdi:thermometer", 1494 | 0, 1495 | SensorStateClass.MEASUREMENT, 1496 | ), 1497 | TYPE_WN34TEMP2C: ( 1498 | "Soil Temperature 2", 1499 | UnitOfTemperature.CELSIUS, 1500 | TYPE_SENSOR, 1501 | SensorDeviceClass.TEMPERATURE, 1502 | "mdi:thermometer", 1503 | 0, 1504 | SensorStateClass.MEASUREMENT, 1505 | ), 1506 | TYPE_WN34TEMP3C: ( 1507 | "Soil Temperature 3", 1508 | UnitOfTemperature.CELSIUS, 1509 | TYPE_SENSOR, 1510 | SensorDeviceClass.TEMPERATURE, 1511 | "mdi:thermometer", 1512 | 0, 1513 | SensorStateClass.MEASUREMENT, 1514 | ), 1515 | TYPE_WN34TEMP4C: ( 1516 | "Soil Temperature 4", 1517 | UnitOfTemperature.CELSIUS, 1518 | TYPE_SENSOR, 1519 | SensorDeviceClass.TEMPERATURE, 1520 | "mdi:thermometer", 1521 | 0, 1522 | SensorStateClass.MEASUREMENT, 1523 | ), 1524 | TYPE_WN34TEMP5C: ( 1525 | "Soil Temperature 5", 1526 | UnitOfTemperature.CELSIUS, 1527 | TYPE_SENSOR, 1528 | SensorDeviceClass.TEMPERATURE, 1529 | "mdi:thermometer", 1530 | 0, 1531 | SensorStateClass.MEASUREMENT, 1532 | ), 1533 | TYPE_WN34TEMP6C: ( 1534 | "Soil Temperature 6", 1535 | UnitOfTemperature.CELSIUS, 1536 | TYPE_SENSOR, 1537 | SensorDeviceClass.TEMPERATURE, 1538 | "mdi:thermometer", 1539 | 0, 1540 | SensorStateClass.MEASUREMENT, 1541 | ), 1542 | TYPE_WN34TEMP7C: ( 1543 | "Soil Temperature 7", 1544 | UnitOfTemperature.CELSIUS, 1545 | TYPE_SENSOR, 1546 | SensorDeviceClass.TEMPERATURE, 1547 | "mdi:thermometer", 1548 | 0, 1549 | SensorStateClass.MEASUREMENT, 1550 | ), 1551 | TYPE_WN34TEMP8C: ( 1552 | "Soil Temperature 8", 1553 | UnitOfTemperature.CELSIUS, 1554 | TYPE_SENSOR, 1555 | SensorDeviceClass.TEMPERATURE, 1556 | "mdi:thermometer", 1557 | 0, 1558 | SensorStateClass.MEASUREMENT, 1559 | ), 1560 | TYPE_WN34BATT1: ( 1561 | "Soil Temperature 1 Battery", 1562 | UnitOfElectricPotential.VOLT, 1563 | TYPE_SENSOR, 1564 | SensorDeviceClass.VOLTAGE, 1565 | "mdi:battery", 1566 | 0, 1567 | SensorStateClass.MEASUREMENT, 1568 | ), 1569 | TYPE_WN34BATT2: ( 1570 | "Soil Temperature 2 Battery", 1571 | UnitOfElectricPotential.VOLT, 1572 | TYPE_SENSOR, 1573 | SensorDeviceClass.VOLTAGE, 1574 | "mdi:battery", 1575 | 0, 1576 | SensorStateClass.MEASUREMENT, 1577 | ), 1578 | TYPE_WN34BATT3: ( 1579 | "Soil Temperature 3 Battery", 1580 | UnitOfElectricPotential.VOLT, 1581 | TYPE_SENSOR, 1582 | SensorDeviceClass.VOLTAGE, 1583 | "mdi:battery", 1584 | 0, 1585 | SensorStateClass.MEASUREMENT, 1586 | ), 1587 | TYPE_WN34BATT4: ( 1588 | "Soil Temperature 4 Battery", 1589 | UnitOfElectricPotential.VOLT, 1590 | TYPE_SENSOR, 1591 | SensorDeviceClass.VOLTAGE, 1592 | "mdi:battery", 1593 | 0, 1594 | SensorStateClass.MEASUREMENT, 1595 | ), 1596 | TYPE_WN34BATT5: ( 1597 | "Soil Temperature 5 Battery", 1598 | UnitOfElectricPotential.VOLT, 1599 | TYPE_SENSOR, 1600 | SensorDeviceClass.VOLTAGE, 1601 | "mdi:battery", 1602 | 0, 1603 | SensorStateClass.MEASUREMENT, 1604 | ), 1605 | TYPE_WN34BATT6: ( 1606 | "Soil Temperature 6 Battery", 1607 | UnitOfElectricPotential.VOLT, 1608 | TYPE_SENSOR, 1609 | SensorDeviceClass.VOLTAGE, 1610 | "mdi:battery", 1611 | 0, 1612 | SensorStateClass.MEASUREMENT, 1613 | ), 1614 | TYPE_WN34BATT7: ( 1615 | "Soil Temperature 7 Battery", 1616 | UnitOfElectricPotential.VOLT, 1617 | TYPE_SENSOR, 1618 | SensorDeviceClass.VOLTAGE, 1619 | "mdi:battery", 1620 | 0, 1621 | SensorStateClass.MEASUREMENT, 1622 | ), 1623 | TYPE_WN34BATT8: ( 1624 | "Soil Temperature 8 Battery", 1625 | UnitOfElectricPotential.VOLT, 1626 | TYPE_SENSOR, 1627 | SensorDeviceClass.VOLTAGE, 1628 | "mdi:battery", 1629 | 0, 1630 | SensorStateClass.MEASUREMENT, 1631 | ), 1632 | TYPE_VPD: ( 1633 | "Vapor Pressure Deficit", 1634 | UnitOfPressure.KPA, 1635 | TYPE_SENSOR, 1636 | SensorDeviceClass.PRESSURE, 1637 | "mdi:gauge", 1638 | S_METRIC, 1639 | SensorStateClass.MEASUREMENT, 1640 | ), 1641 | } 1642 | 1643 | IGNORED_SENSORS = [ 1644 | "tempinf", 1645 | "tempf", 1646 | "temp1f", 1647 | "temp2f", 1648 | "temp3f", 1649 | "temp4f", 1650 | "temp5f", 1651 | "temp6f", 1652 | "temp7f", 1653 | "temp8f", 1654 | "tf_co2", 1655 | "tf_ch1", 1656 | "tf_ch2", 1657 | "tf_ch3", 1658 | "tf_ch4", 1659 | "tf_ch5", 1660 | "tf_ch6", 1661 | "tf_ch7", 1662 | "tf_ch8", 1663 | "dateutc", 1664 | "windchillf", 1665 | "dewpointf", 1666 | "dewpointinf", 1667 | "dewpoint1f", 1668 | "dewpoint2f", 1669 | "dewpoint3f", 1670 | "dewpoint4f", 1671 | "dewpoint5f", 1672 | "dewpoint6f", 1673 | "dewpoint7f", 1674 | "dewpoint8f", 1675 | "mac", 1676 | "fields", 1677 | "runtime", 1678 | DATA_PASSKEY, 1679 | DATA_STATIONTYPE, 1680 | DATA_FREQ, 1681 | DATA_MODEL, 1682 | ] 1683 | --------------------------------------------------------------------------------