├── md.images └── overview.png ├── hacs.json ├── custom_components └── ecowitt │ ├── manifest.json │ ├── schemas.py │ ├── translations │ └── en.json │ ├── binary_sensor.py │ ├── sensor.py │ ├── config_flow.py │ ├── __init__.py │ └── const.py ├── .github └── workflows │ └── validate.yml ├── .gitignore ├── README.md └── LICENSE /md.images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garbled1/homeassistant_ecowitt/HEAD/md.images/overview.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ecowitt Weather Station", 3 | "render_readme": true, 4 | "domains": ["sensor", "binary_sensor"], 5 | "iot_class": "Local Push" 6 | } 7 | -------------------------------------------------------------------------------- /custom_components/ecowitt/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "ecowitt", 3 | "name": "Ecowitt Weather Station", 4 | "version": "0.8", 5 | "config_flow": true, 6 | "documentation": "https://www.home-assistant.io/integrations/ecowitt", 7 | "requirements": [ 8 | "pyecowitt==0.14" 9 | ], 10 | "ssdp": [], 11 | "zeroconf": [], 12 | "homekit": {}, 13 | "dependencies": [], 14 | "codeowners": [ 15 | "@garbled1" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - name: HACS validation 15 | uses: "hacs/integration/action@master" 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | CATEGORY: "integration" 19 | 20 | env: 21 | SKIP_BRANDS_CHECK: "True" 22 | -------------------------------------------------------------------------------- /custom_components/ecowitt/schemas.py: -------------------------------------------------------------------------------- 1 | """Schema definitions""" 2 | import voluptuous as vol 3 | import homeassistant.helpers.config_validation as cv 4 | 5 | from .const import ( 6 | CONF_UNIT_BARO, 7 | CONF_UNIT_WIND, 8 | CONF_UNIT_RAIN, 9 | CONF_UNIT_WINDCHILL, 10 | CONF_UNIT_LIGHTNING, 11 | CONF_NAME, 12 | DEFAULT_PORT, 13 | DOMAIN, 14 | W_TYPE_HYBRID, 15 | ) 16 | from homeassistant.const import ( 17 | CONF_PORT, 18 | CONF_UNIT_SYSTEM_METRIC, 19 | CONF_UNIT_SYSTEM_IMPERIAL, 20 | ) 21 | 22 | COMPONENT_SCHEMA = vol.Schema( 23 | { 24 | vol.Required(CONF_PORT): cv.port, 25 | vol.Optional(CONF_UNIT_BARO, 26 | default=CONF_UNIT_SYSTEM_METRIC): cv.string, 27 | vol.Optional(CONF_UNIT_WIND, 28 | default=CONF_UNIT_SYSTEM_IMPERIAL): cv.string, 29 | vol.Optional(CONF_UNIT_RAIN, 30 | default=CONF_UNIT_SYSTEM_IMPERIAL): cv.string, 31 | vol.Optional(CONF_UNIT_LIGHTNING, 32 | default=CONF_UNIT_SYSTEM_IMPERIAL): cv.string, 33 | vol.Optional(CONF_UNIT_WINDCHILL, 34 | default=W_TYPE_HYBRID): cv.string, 35 | } 36 | ) 37 | 38 | CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA) 39 | 40 | DATA_SCHEMA = vol.Schema( 41 | { 42 | vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, 43 | vol.Optional(CONF_NAME, 44 | description={"suggested_value": "ecowitt"}): str, 45 | } 46 | ) 47 | -------------------------------------------------------------------------------- /custom_components/ecowitt/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "Ecowitt Weather Station", 4 | "abort": { 5 | "already_configured": "Device is already configured.", 6 | "unknown": "Unknown error." 7 | }, 8 | "step": { 9 | "user": { 10 | "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.", 11 | "title": "Instructions for setting up the Ecowitt." 12 | }, 13 | "initial_options": { 14 | "title": "Ecowitt Parameters", 15 | "data": { 16 | "port": "Listening port" 17 | } 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "barounit": "Barometer Unit", 26 | "windunit": "Wind Unit", 27 | "rainunit": "Rainfall Unit", 28 | "lightningunit": "Lightning distance unit", 29 | "windchillunit": "Windchill calculation" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /custom_components/ecowitt/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Support for Ecowitt Weather Stations.""" 2 | import logging 3 | 4 | from . import EcowittEntity, async_add_ecowitt_entities 5 | from .const import ( 6 | DOMAIN, 7 | TYPE_BINARY_SENSOR, 8 | REG_ENTITIES, 9 | SIGNAL_ADD_ENTITIES, 10 | ) 11 | from homeassistant.components.binary_sensor import BinarySensorEntity 12 | from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN 13 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 14 | from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | async def async_setup_entry(hass, entry, async_add_entities): 20 | """Add sensors if new.""" 21 | 22 | def add_entities(discovery_info=None): 23 | async_add_ecowitt_entities(hass, entry, EcowittBinarySensor, 24 | BINARY_SENSOR_DOMAIN, async_add_entities, 25 | discovery_info) 26 | 27 | signal = f"{SIGNAL_ADD_ENTITIES}_{BINARY_SENSOR_DOMAIN}" 28 | async_dispatcher_connect(hass, signal, add_entities) 29 | add_entities(hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][TYPE_BINARY_SENSOR]) 30 | 31 | 32 | class EcowittBinarySensor(EcowittEntity, BinarySensorEntity): 33 | 34 | def __init__(self, hass, entry, key, name, dc, uom, icon, sc): 35 | """Initialize the sensor.""" 36 | super().__init__(hass, entry, key, name) 37 | self._icon = icon 38 | self._uom = uom 39 | self._dc = dc 40 | 41 | @property 42 | def is_on(self): 43 | """Return true if the binary sensor is on.""" 44 | if self._key in self._ws.last_values: 45 | if self._ws.last_values[self._key] > 0: 46 | return True 47 | else: 48 | _LOGGER.warning("Sensor %s not in last update, check range or battery", 49 | self._key) 50 | return None 51 | return False 52 | 53 | @property 54 | def state(self): 55 | """Return the state of the binary sensor.""" 56 | # Don't claim a leak is cleared if the sensor is out of range 57 | if self.is_on is None: 58 | return STATE_UNKNOWN 59 | return STATE_ON if self.is_on else STATE_OFF 60 | 61 | @property 62 | def device_class(self): 63 | """Return the device class.""" 64 | return self._dc 65 | -------------------------------------------------------------------------------- /custom_components/ecowitt/sensor.py: -------------------------------------------------------------------------------- 1 | """Support for Ecowitt Weather Stations.""" 2 | import logging 3 | import homeassistant.util.dt as dt_util 4 | 5 | from . import EcowittEntity, async_add_ecowitt_entities 6 | from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN 7 | from homeassistant.components.sensor import SensorEntity 8 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 9 | from .const import ( 10 | DOMAIN, 11 | TYPE_SENSOR, 12 | REG_ENTITIES, 13 | SIGNAL_ADD_ENTITIES, 14 | ) 15 | 16 | from homeassistant.const import ( 17 | STATE_UNKNOWN, 18 | DEVICE_CLASS_TIMESTAMP, 19 | DEVICE_CLASS_BATTERY, 20 | PERCENTAGE, 21 | ) 22 | 23 | _LOGGER = logging.getLogger(__name__) 24 | 25 | 26 | async def async_setup_entry(hass, entry, async_add_entities): 27 | """Add sensors if new.""" 28 | 29 | def add_entities(discovery_info=None): 30 | async_add_ecowitt_entities(hass, entry, EcowittSensor, 31 | SENSOR_DOMAIN, async_add_entities, 32 | discovery_info) 33 | 34 | signal = f"{SIGNAL_ADD_ENTITIES}_{SENSOR_DOMAIN}" 35 | async_dispatcher_connect(hass, signal, add_entities) 36 | add_entities(hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][TYPE_SENSOR]) 37 | 38 | 39 | class EcowittSensor(EcowittEntity, SensorEntity): 40 | 41 | def __init__(self, hass, entry, key, name, dc, uom, icon, sc): 42 | """Initialize the sensor.""" 43 | super().__init__(hass, entry, key, name) 44 | self._icon = icon 45 | self._uom = uom 46 | self._dc = dc 47 | self._sc = sc 48 | 49 | @property 50 | def native_value(self): 51 | """Return the state of the sensor.""" 52 | if self._key in self._ws.last_values: 53 | # The lightning time is reported in UTC, hooray. 54 | if self._dc == DEVICE_CLASS_TIMESTAMP: 55 | if not isinstance(self._ws.last_values[self._key], int): 56 | return STATE_UNKNOWN 57 | return dt_util.as_local( 58 | dt_util.utc_from_timestamp(self._ws.last_values[self._key]) 59 | ).isoformat() 60 | # Battery value is 0-5 61 | if self._dc == DEVICE_CLASS_BATTERY and self._uom == PERCENTAGE: 62 | return self._ws.last_values[self._key] * 20.0 63 | return self._ws.last_values[self._key] 64 | _LOGGER.warning("Sensor %s not in last update, check range or battery", 65 | self._key) 66 | return STATE_UNKNOWN 67 | 68 | @property 69 | def native_unit_of_measurement(self): 70 | """Return the unit of measurement.""" 71 | return self._uom 72 | 73 | @property 74 | def icon(self): 75 | """Return the icon to use in the fronend.""" 76 | return self._icon 77 | 78 | @property 79 | def device_class(self): 80 | """Return the device class.""" 81 | return self._dc 82 | 83 | @property 84 | def state_class(self): 85 | """Return sensor state class.""" 86 | return self._sc 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -*- mode: gitignore; -*- 2 | *~ 3 | \#*\# 4 | /.emacs.desktop 5 | /.emacs.desktop.lock 6 | *.elc 7 | auto-save-list 8 | tramp 9 | .\#* 10 | 11 | # Org-mode 12 | .org-id-locations 13 | *_archive 14 | 15 | # flymake-mode 16 | *_flymake.* 17 | 18 | # eshell files 19 | /eshell/history 20 | /eshell/lastdir 21 | 22 | # elpa packages 23 | /elpa/ 24 | 25 | # reftex files 26 | *.rel 27 | 28 | # AUCTeX auto folder 29 | /auto/ 30 | 31 | # cask packages 32 | .cask/ 33 | dist/ 34 | 35 | # Flycheck 36 | flycheck_*.el 37 | 38 | # server auth directory 39 | /server/ 40 | 41 | # projectiles files 42 | .projectile 43 | 44 | # directory configuration 45 | .dir-locals.el 46 | 47 | # network security 48 | /network-security.data 49 | 50 | 51 | # Byte-compiled / optimized / DLL files 52 | __pycache__/ 53 | *.py[cod] 54 | *$py.class 55 | 56 | # C extensions 57 | *.so 58 | 59 | # Distribution / packaging 60 | .Python 61 | build/ 62 | develop-eggs/ 63 | dist/ 64 | downloads/ 65 | eggs/ 66 | .eggs/ 67 | lib/ 68 | lib64/ 69 | parts/ 70 | sdist/ 71 | var/ 72 | wheels/ 73 | pip-wheel-metadata/ 74 | share/python-wheels/ 75 | *.egg-info/ 76 | .installed.cfg 77 | *.egg 78 | MANIFEST 79 | 80 | # PyInstaller 81 | # Usually these files are written by a python script from a template 82 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 83 | *.manifest 84 | *.spec 85 | 86 | # Installer logs 87 | pip-log.txt 88 | pip-delete-this-directory.txt 89 | 90 | # Unit test / coverage reports 91 | htmlcov/ 92 | .tox/ 93 | .nox/ 94 | .coverage 95 | .coverage.* 96 | .cache 97 | nosetests.xml 98 | coverage.xml 99 | *.cover 100 | *.py,cover 101 | .hypothesis/ 102 | .pytest_cache/ 103 | 104 | # Translations 105 | *.mo 106 | *.pot 107 | 108 | # Django stuff: 109 | *.log 110 | local_settings.py 111 | db.sqlite3 112 | db.sqlite3-journal 113 | 114 | # Flask stuff: 115 | instance/ 116 | .webassets-cache 117 | 118 | # Scrapy stuff: 119 | .scrapy 120 | 121 | # Sphinx documentation 122 | docs/_build/ 123 | 124 | # PyBuilder 125 | target/ 126 | 127 | # Jupyter Notebook 128 | .ipynb_checkpoints 129 | 130 | # IPython 131 | profile_default/ 132 | ipython_config.py 133 | 134 | # pyenv 135 | .python-version 136 | 137 | # pipenv 138 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 139 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 140 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 141 | # install all needed dependencies. 142 | #Pipfile.lock 143 | 144 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 145 | __pypackages__/ 146 | 147 | # Celery stuff 148 | celerybeat-schedule 149 | celerybeat.pid 150 | 151 | # SageMath parsed files 152 | *.sage.py 153 | 154 | # Environments 155 | .env 156 | .venv 157 | env/ 158 | venv/ 159 | ENV/ 160 | env.bak/ 161 | venv.bak/ 162 | 163 | # Spyder project settings 164 | .spyderproject 165 | .spyproject 166 | 167 | # Rope project settings 168 | .ropeproject 169 | 170 | # mkdocs documentation 171 | /site 172 | 173 | # mypy 174 | .mypy_cache/ 175 | .dmypy.json 176 | dmypy.json 177 | 178 | # Pyre type checker 179 | .pyre/ 180 | -------------------------------------------------------------------------------- /custom_components/ecowitt/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for ecowitt.""" 2 | import logging 3 | 4 | import voluptuous as vol 5 | from homeassistant import config_entries, core, exceptions 6 | from homeassistant.core import callback 7 | 8 | from homeassistant.const import ( 9 | CONF_PORT, 10 | CONF_UNIT_SYSTEM_METRIC, 11 | CONF_UNIT_SYSTEM_IMPERIAL, 12 | ) 13 | 14 | from .const import ( 15 | CONF_UNIT_BARO, 16 | CONF_UNIT_WIND, 17 | CONF_UNIT_RAIN, 18 | CONF_UNIT_WINDCHILL, 19 | CONF_UNIT_LIGHTNING, 20 | DOMAIN, 21 | W_TYPE_HYBRID, 22 | UNIT_OPTS, 23 | WIND_OPTS, 24 | WINDCHILL_OPTS 25 | ) 26 | 27 | from .schemas import ( 28 | DATA_SCHEMA, 29 | ) 30 | 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | 35 | async def validate_input(hass: core.HomeAssistant, data): 36 | """Validate user input.""" 37 | for entry in hass.config_entries.async_entries(DOMAIN): 38 | if entry.data[CONF_PORT] == data[CONF_PORT]: 39 | raise AlreadyConfigured 40 | return {"title": f"Ecowitt on port {data[CONF_PORT]}"} 41 | 42 | 43 | class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 44 | """Config flow for the Ecowitt.""" 45 | 46 | VERSION = 1 47 | CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN 48 | 49 | async def async_step_import(self, device_config): 50 | """Import a configuration.yaml config, if any.""" 51 | try: 52 | await validate_input(self.hass, device_config) 53 | except AlreadyConfigured: 54 | return self.async_abort(reason="already_configured") 55 | 56 | port = device_config[CONF_PORT] 57 | return self.async_create_entry( 58 | title=f"Ecowitt on port {port}", 59 | data=device_config 60 | ) 61 | 62 | async def async_step_user(self, user_input=None): 63 | """Give initial instructions for setup.""" 64 | if user_input is not None: 65 | return await self.async_step_initial_options() 66 | 67 | return self.async_show_form(step_id="user") 68 | 69 | async def async_step_initial_options(self, user_input=None): 70 | """Ask the user for the setup options.""" 71 | errors = {} 72 | if user_input is not None: 73 | try: 74 | info = await validate_input(self.hass, user_input) 75 | return self.async_create_entry(title=info["title"], 76 | data=user_input) 77 | except AlreadyConfigured: 78 | return self.async_abort(reason="already_configured") 79 | 80 | return self.async_show_form( 81 | step_id="initial_options", data_schema=DATA_SCHEMA, errors=errors 82 | ) 83 | 84 | @staticmethod 85 | @callback 86 | def async_get_options_flow(config_entry): 87 | return EcowittOptionsFlowHandler(config_entry) 88 | 89 | 90 | class AlreadyConfigured(exceptions.HomeAssistantError): 91 | """Error to indicate this device is already configured.""" 92 | 93 | 94 | class EcowittOptionsFlowHandler(config_entries.OptionsFlow): 95 | """Ecowitt config flow options handler.""" 96 | 97 | def __init__(self, config_entry): 98 | """Initialize HASS options flow.""" 99 | self.config_entry = config_entry 100 | 101 | async def async_step_init(self, user_input=None): 102 | """Handle a flow initialized by the user.""" 103 | if user_input is not None: 104 | return self.async_create_entry(title="", data=user_input) 105 | 106 | options_schema = vol.Schema( 107 | { 108 | vol.Optional( 109 | CONF_UNIT_BARO, 110 | default=self.config_entry.options.get( 111 | CONF_UNIT_BARO, CONF_UNIT_SYSTEM_METRIC, 112 | ), 113 | ): vol.In(UNIT_OPTS), 114 | vol.Optional( 115 | CONF_UNIT_WIND, 116 | default=self.config_entry.options.get( 117 | CONF_UNIT_WIND, CONF_UNIT_SYSTEM_IMPERIAL, 118 | ), 119 | ): vol.In(WIND_OPTS), 120 | vol.Optional( 121 | CONF_UNIT_RAIN, 122 | default=self.config_entry.options.get( 123 | CONF_UNIT_RAIN, CONF_UNIT_SYSTEM_IMPERIAL, 124 | ), 125 | ): vol.In(UNIT_OPTS), 126 | vol.Optional( 127 | CONF_UNIT_LIGHTNING, 128 | default=self.config_entry.options.get( 129 | CONF_UNIT_LIGHTNING, CONF_UNIT_SYSTEM_IMPERIAL, 130 | ), 131 | ): vol.In(UNIT_OPTS), 132 | vol.Optional( 133 | CONF_UNIT_WINDCHILL, 134 | default=self.config_entry.options.get( 135 | CONF_UNIT_WINDCHILL, W_TYPE_HYBRID, 136 | ), 137 | ): vol.In(WINDCHILL_OPTS), 138 | } 139 | ) 140 | return self.async_show_form(step_id="init", data_schema=options_schema) 141 | -------------------------------------------------------------------------------- /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 | Ecowitt driver for homeassistant 5 | 6 | ![Bling](https://raw.githubusercontent.com/garbled1/homeassistant_ecowitt/master/md.images/overview.png) 7 | 8 | ## Configuration: 9 | 10 | Configuration for the Ecowitt integration is now performed via a config flow 11 | as opposed to yaml configuration file. 12 | 13 | 1. Go to HACS -> Integrations -> Click "+" 14 | 1. Search for "Ecowitt Weather Station" repository and add to HACS 15 | 1. Restart Home Assistant when it says to. 16 | 1. In Home Assistant, go to Configuration -> Integrations -> Click "+ Add Integration" 17 | 1. Search for "Ecowitt Weather Station" and follow the instructions to setup. 18 | 19 | Ecowitt should now appear as a card under the HA Integrations page 20 | with "Options" selection available at the bottom of the card. 21 | 22 | You must select the port when enabling, see below section on "How to set up". 23 | 24 | There are a few options available once the integration is setup, these are 25 | available in the "options" dialog in the integrations box for the component. 26 | 27 | * Barometer Unit (default metric) 28 | * Wind Unit (default imperial) 29 | * Rainfall Unit (default imperial) 30 | * Lightning Unit (default imperial) 31 | * Windchill Unit (default hybrid) 32 | 33 | Windchill can be one of "hybrid", "old", or "new". 34 | Defaults for units are as shown above. 35 | Units can be one of "metric" or "imperial". 36 | 37 | Note that if you change the units, it will create a new sensor for the 38 | different unit. 39 | For example, if you had wind set to imperial, "sensor.wind_speed" 40 | would have been your data entity, but switching to metric will create a 41 | "sensor.wind_speed_2". 42 | You will see in the entities page the original "sensor.wind_speed" will be 43 | marked with a status of "Restored". 44 | You can safely delete the old sensor once you validate you are seeing data 45 | on the new one. 46 | Be sure to update any automations/etc that reference the old sensor. 47 | 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 | 83 | ## How to set up: 84 | 85 | Use the WS View app (on your phone) for your Ecowitt device, and connect to it. 86 | 87 | 1. Pick menu -> device list -> Pick your station. 88 | 1. Hit next repeatedly to move to the last screen titled "Customized" 89 | 1. Pick the protocol Ecowitt, and put in the ip/hostname of your hass server. 90 | 1. Path doesn't matter as long as it ends in /, leave the default, or change it to 91 | just /. 92 | 1. Pick a port that is not in use on the server (netstat -lt). 93 | (4199 is probably a good default) 94 | 1. Pick a reasonable value for updates, like 60 seconds. 95 | 1. Save configuration. 96 | 97 | The Ecowitt should then start attempting to send data to your server. 98 | 99 | In home assistant, navigate to integrations, and search for the ecowitt component. 100 | You will need to supply a port, and an optional name for the station (if you have 101 | multiple stations this might be useful) 102 | Pick the same port you did in the wsview app. 103 | 104 | One note: You may wish to setup the integration, and change the options for 105 | the various units prior to setting up the physical device. 106 | This will prevent creation of any entities for the wrong measurement unit from 107 | being created if you decide to change one to a non-default. 108 | 109 | ## Errors in the logs 110 | 111 | If you get an error in the logs about an unhandled sensor, open an issue and 112 | paste the log message so I can add the sensor. 113 | 114 | If you have a sensor that is barely in range, you will see a bunch of messages 115 | in the logs about the sensor not being in the most recent update. 116 | This can also be caused by a sensor that has a low battery. 117 | If you know this sensor is just badly placed, you can ignore these, but if you 118 | start seeing them for previously reliable sensors, check the batteries. 119 | 120 | 121 | ## Delay on startup 122 | 123 | Older versions of this component would cause homeassistant to freeze on startup 124 | waiting for the first sensor burst. 125 | This is no longer the case. 126 | Sensors will now show up as restored until the first data packet is recieved 127 | from the ecowitt. 128 | There should be no delay on startup at all. 129 | 130 | 131 | ## A note on leak sensors 132 | 133 | Because leak sensors may be potentially important devices for automation, 134 | they handle going out of range somewhat differently. 135 | If a leak sensor is missing from the last data update from the ecowitt, it 136 | will go into state Unknown. 137 | If you rely upon a leak sensor for something vital, I suggest testing your 138 | automation, by disconnecting the battery from the sensor, and validating 139 | your code does something sane. 140 | 141 | 142 | ## I want a pretty card for my weather 143 | 144 | I highly reccomend https://github.com/r-renato/ha-card-weather-conditions 145 | It's fairly simple to setup as a custom card, and produces lovely results. 146 | You can easily set it up to combine local data from your sensors, with 147 | forcast data from external sources for sensors you don't have 148 | (like pollen counts, for example). 149 | 150 | This is a copy of my setup. 151 | Sensors named with the sensor.cc_ are from the climacell external source, 152 | other sensors are my local weatherstation. 153 | 154 | ``` 155 | air_quality: 156 | co: sensor.cc_co 157 | epa_aqi: sensor.cc_epa_aqi 158 | epa_health_concern: sensor.cc_epa_health_concern 159 | no2: sensor.cc_no2 160 | o3: sensor.cc_o3 161 | pm10: sensor.cc_pm10 162 | pm25: sensor.pm2_5_1 163 | so2: sensor.cc_so2 164 | animation: true 165 | name: WeatherStation 166 | pollen: 167 | grass: 168 | entity: sensor.cc_pollen_grass 169 | high: 3 170 | low: 1 171 | max: 5 172 | min: 0 173 | tree: 174 | entity: sensor.cc_pollen_tree 175 | high: 3 176 | low: 1 177 | max: 5 178 | min: 0 179 | weed: 180 | entity: sensor.cc_pollen_weed 181 | high: 3 182 | low: 1 183 | max: 5 184 | min: 0 185 | type: 'custom:ha-card-weather-conditions' 186 | weather: 187 | current: 188 | current_conditions: sensor.cc_weather_condition 189 | feels_like: sensor.windchill 190 | forecast: true 191 | humidity: sensor.humidity 192 | precipitation: sensor.rain_rate 193 | pressure: sensor.absolute_pressure 194 | sun: sun.sun 195 | temperature: sensor.outdoor_temperature 196 | visibility: sensor.cc_visibility 197 | wind_bearing: sensor.wind_direction 198 | wind_speed: sensor.wind_speed 199 | forecast: 200 | icons: 201 | day_1: sensor.cc_weather_condition_0d 202 | day_2: sensor.cc_weather_condition_1d 203 | day_3: sensor.cc_weather_condition_2d 204 | day_4: sensor.cc_weather_condition_3d 205 | day_5: sensor.cc_weather_condition_4d 206 | precipitation_intensity: 207 | day_1: sensor.cc_max_precipitation_0d 208 | day_2: sensor.cc_max_precipitation_1d 209 | day_3: sensor.cc_max_precipitation_2d 210 | day_4: sensor.cc_max_precipitation_3d 211 | day_5: sensor.cc_max_precipitation_4d 212 | precipitation_probability: 213 | day_1: sensor.cc_precipitation_probability_0d 214 | day_2: sensor.cc_precipitation_probability_1d 215 | day_3: sensor.cc_precipitation_probability_2d 216 | day_4: sensor.cc_precipitation_probability_3d 217 | day_5: sensor.cc_precipitation_probability_4d 218 | temperature_high: 219 | day_1: sensor.cc_max_temperature_0d 220 | day_2: sensor.cc_max_temperature_1d 221 | day_3: sensor.cc_max_temperature_2d 222 | day_4: sensor.cc_max_temperature_3d 223 | day_5: sensor.cc_max_temperature_4d 224 | temperature_low: 225 | day_1: sensor.cc_min_temperature_0d 226 | day_2: sensor.cc_min_temperature_1d 227 | day_3: sensor.cc_min_temperature_2d 228 | day_4: sensor.cc_min_temperature_3d 229 | day_5: sensor.cc_min_temperature_4d 230 | icons_model: climacell 231 | ``` 232 | -------------------------------------------------------------------------------- /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/ecowitt/__init__.py: -------------------------------------------------------------------------------- 1 | """The Ecowitt Weather Station Component.""" 2 | import asyncio 3 | import logging 4 | import time 5 | 6 | from pyecowitt import ( 7 | EcoWittListener, 8 | WINDCHILL_OLD, 9 | WINDCHILL_NEW, 10 | WINDCHILL_HYBRID, 11 | ) 12 | 13 | from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry 14 | from homeassistant.core import HomeAssistant, callback 15 | from homeassistant.helpers.dispatcher import ( 16 | async_dispatcher_connect, 17 | async_dispatcher_send, 18 | ) 19 | from homeassistant.helpers.entity import Entity 20 | from homeassistant.helpers.entity_registry import ( 21 | async_get_registry as async_get_entity_registry, 22 | ) 23 | 24 | from homeassistant.const import ( 25 | CONF_PORT, 26 | CONF_UNIT_SYSTEM_METRIC, 27 | CONF_UNIT_SYSTEM_IMPERIAL, 28 | ) 29 | 30 | from .const import ( 31 | CONF_UNIT_BARO, 32 | CONF_UNIT_WIND, 33 | CONF_UNIT_RAIN, 34 | CONF_UNIT_WINDCHILL, 35 | CONF_UNIT_LIGHTNING, 36 | CONF_UNIT_SYSTEM_METRIC_MS, 37 | CONF_NAME, 38 | DOMAIN, 39 | DATA_ECOWITT, 40 | DATA_STATION, 41 | DATA_PASSKEY, 42 | DATA_STATIONTYPE, 43 | DATA_FREQ, 44 | DATA_MODEL, 45 | DATA_READY, 46 | DATA_OPTIONS, 47 | ECOWITT_PLATFORMS, 48 | IGNORED_SENSORS, 49 | S_IMPERIAL, 50 | S_METRIC, 51 | S_METRIC_MS, 52 | SENSOR_TYPES, 53 | TYPE_SENSOR, 54 | TYPE_BINARY_SENSOR, 55 | W_TYPE_NEW, 56 | W_TYPE_OLD, 57 | W_TYPE_HYBRID, 58 | REG_ENTITIES, 59 | SIGNAL_ADD_ENTITIES, 60 | SIGNAL_REMOVE_ENTITIES, 61 | ) 62 | 63 | NOTIFICATION_ID = DOMAIN 64 | NOTIFICATION_TITLE = "Ecowitt config migrated" 65 | 66 | _LOGGER = logging.getLogger(__name__) 67 | 68 | 69 | async def async_setup(hass: HomeAssistant, config: dict): 70 | """Configure the Ecowitt component using YAML.""" 71 | hass.data.setdefault(DOMAIN, {}) 72 | 73 | if DOMAIN in config: 74 | data = { 75 | CONF_PORT: config[DOMAIN][CONF_PORT], 76 | CONF_NAME: None, 77 | } 78 | # set defaults if not set in conf 79 | if CONF_UNIT_BARO not in config[DOMAIN]: 80 | config[DOMAIN][CONF_UNIT_BARO] = CONF_UNIT_SYSTEM_METRIC 81 | if CONF_UNIT_WIND not in config[DOMAIN]: 82 | config[DOMAIN][CONF_UNIT_WIND] = CONF_UNIT_SYSTEM_IMPERIAL 83 | if CONF_UNIT_RAIN not in config[DOMAIN]: 84 | config[DOMAIN][CONF_UNIT_RAIN] = CONF_UNIT_SYSTEM_IMPERIAL 85 | if CONF_UNIT_LIGHTNING not in config[DOMAIN]: 86 | config[DOMAIN][CONF_UNIT_LIGHTNING] = CONF_UNIT_SYSTEM_IMPERIAL 87 | if CONF_UNIT_WINDCHILL not in config[DOMAIN]: 88 | config[DOMAIN][CONF_UNIT_WINDCHILL] = W_TYPE_HYBRID 89 | # set the options for migration 90 | hass.data[DOMAIN][DATA_OPTIONS] = { 91 | CONF_UNIT_BARO: config[DOMAIN][CONF_UNIT_BARO], 92 | CONF_UNIT_WIND: config[DOMAIN][CONF_UNIT_WIND], 93 | CONF_UNIT_RAIN: config[DOMAIN][CONF_UNIT_RAIN], 94 | CONF_UNIT_LIGHTNING: config[DOMAIN][CONF_UNIT_LIGHTNING], 95 | CONF_UNIT_WINDCHILL: config[DOMAIN][CONF_UNIT_WINDCHILL], 96 | } 97 | hass.components.persistent_notification.create( 98 | "Ecowitt configuration has been migrated from yaml format " 99 | "to a config_flow. Your options and settings should have been " 100 | "migrated automatically. Verify them in the Configuration -> " 101 | "Integrations menu, and then delete the ecowitt section from " 102 | "your yaml file.", 103 | title=NOTIFICATION_TITLE, 104 | notification_id=NOTIFICATION_ID, 105 | ) 106 | hass.async_create_task( 107 | hass.config_entries.flow.async_init( 108 | DOMAIN, context={"source": SOURCE_IMPORT}, data=data 109 | ) 110 | ) 111 | return True 112 | 113 | 114 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 115 | """Set up the Ecowitt component from UI.""" 116 | 117 | if hass.data.get(DOMAIN) is None: 118 | hass.data.setdefault(DOMAIN, {}) 119 | 120 | # if options existed in the YAML but not in the config entry, add 121 | if (not entry.options 122 | and entry.source == SOURCE_IMPORT 123 | and hass.data.get(DOMAIN) 124 | and hass.data[DOMAIN].get(DATA_OPTIONS)): 125 | hass.config_entries.async_update_entry( 126 | entry=entry, 127 | options=hass.data[DOMAIN][DATA_OPTIONS], 128 | ) 129 | 130 | # Store config 131 | hass.data[DOMAIN][entry.entry_id] = {} 132 | ecowitt_data = hass.data[DOMAIN][entry.entry_id] 133 | ecowitt_data[DATA_STATION] = {} 134 | ecowitt_data[DATA_READY] = False 135 | ecowitt_data[REG_ENTITIES] = {} 136 | for pl in ECOWITT_PLATFORMS: 137 | ecowitt_data[REG_ENTITIES][pl] = [] 138 | 139 | if not entry.options: 140 | entry.options = { 141 | CONF_UNIT_BARO: CONF_UNIT_SYSTEM_METRIC, 142 | CONF_UNIT_WIND: CONF_UNIT_SYSTEM_IMPERIAL, 143 | CONF_UNIT_RAIN: CONF_UNIT_SYSTEM_IMPERIAL, 144 | CONF_UNIT_LIGHTNING: CONF_UNIT_SYSTEM_IMPERIAL, 145 | CONF_UNIT_WINDCHILL: W_TYPE_HYBRID, 146 | } 147 | 148 | # preload some model info 149 | stationinfo = ecowitt_data[DATA_STATION] 150 | stationinfo[DATA_STATIONTYPE] = "Unknown" 151 | stationinfo[DATA_FREQ] = "Unknown" 152 | stationinfo[DATA_MODEL] = "Unknown" 153 | 154 | # setup the base connection 155 | ws = EcoWittListener(port=entry.data[CONF_PORT]) 156 | ecowitt_data[DATA_ECOWITT] = ws 157 | 158 | if entry.options[CONF_UNIT_WINDCHILL] == W_TYPE_OLD: 159 | ws.set_windchill(WINDCHILL_OLD) 160 | if entry.options[CONF_UNIT_WINDCHILL] == W_TYPE_NEW: 161 | ws.set_windchill(WINDCHILL_NEW) 162 | if entry.options[CONF_UNIT_WINDCHILL] == W_TYPE_HYBRID: 163 | ws.set_windchill(WINDCHILL_HYBRID) 164 | 165 | hass.loop.create_task(ws.listen()) 166 | 167 | async def close_server(*args): 168 | """ Close the ecowitt server.""" 169 | await ws.stop() 170 | 171 | def check_imp_metric_sensor(sensor): 172 | """Check if this is the wrong sensor for our config (imp/metric).""" 173 | # Is this a metric or imperial sensor, lookup and skip 174 | name, uom, kind, device_class, icon, metric, sc = SENSOR_TYPES[sensor] 175 | if metric == 0: 176 | return True 177 | if "baro" in sensor: 178 | if (entry.options[CONF_UNIT_BARO] == CONF_UNIT_SYSTEM_IMPERIAL 179 | and metric == S_METRIC): 180 | return False 181 | if (entry.options[CONF_UNIT_BARO] == CONF_UNIT_SYSTEM_METRIC 182 | and metric == S_IMPERIAL): 183 | return False 184 | if "rain" in sensor: 185 | if (entry.options[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_IMPERIAL 186 | and metric == S_METRIC): 187 | return False 188 | if (entry.options[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_METRIC 189 | and metric == S_IMPERIAL): 190 | return False 191 | if "windchill" not in sensor and ("wind" in sensor or "gust" in sensor): 192 | if (entry.options[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_IMPERIAL 193 | and metric != S_IMPERIAL): 194 | return False 195 | if (entry.options[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_METRIC 196 | and metric != S_METRIC): 197 | return False 198 | if (entry.options[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_METRIC_MS 199 | and metric != S_METRIC_MS): 200 | return False 201 | if (sensor == 'lightning' 202 | and entry.options[CONF_UNIT_LIGHTNING] == CONF_UNIT_SYSTEM_IMPERIAL): 203 | return False 204 | if (sensor == 'lightning_mi' 205 | and entry.options[CONF_UNIT_LIGHTNING] == CONF_UNIT_SYSTEM_METRIC): 206 | return False 207 | return True 208 | 209 | def check_and_append_sensor(sensor): 210 | """Check the sensor for validity, and append to new entitiy list.""" 211 | if sensor not in SENSOR_TYPES: 212 | if sensor not in IGNORED_SENSORS: 213 | _LOGGER.warning("Unhandled sensor type %s", sensor) 214 | return None 215 | 216 | # Is this a metric or imperial sensor, lookup and skip 217 | if not check_imp_metric_sensor(sensor): 218 | return None 219 | 220 | name, uom, kind, device_class, icon, metric, sc = SENSOR_TYPES[sensor] 221 | ecowitt_data[REG_ENTITIES][kind].append(sensor) 222 | return kind 223 | 224 | async def _first_data_rec(weather_data): 225 | _LOGGER.info("First ecowitt data recd, setting up sensors.") 226 | # check if we have model info, etc. 227 | if DATA_PASSKEY in ws.last_values: 228 | stationinfo[DATA_PASSKEY] = ws.last_values[DATA_PASSKEY] 229 | ws.last_values.pop(DATA_PASSKEY, None) 230 | else: 231 | _LOGGER.error("No passkey, cannot set unique id.") 232 | return False 233 | if DATA_STATIONTYPE in ws.last_values: 234 | stationinfo[DATA_STATIONTYPE] = ws.last_values[DATA_STATIONTYPE] 235 | ws.last_values.pop(DATA_STATIONTYPE, None) 236 | if DATA_FREQ in ws.last_values: 237 | stationinfo[DATA_FREQ] = ws.last_values[DATA_FREQ] 238 | ws.last_values.pop(DATA_FREQ, None) 239 | if DATA_MODEL in ws.last_values: 240 | stationinfo[DATA_MODEL] = ws.last_values[DATA_MODEL] 241 | ws.last_values.pop(DATA_MODEL, None) 242 | 243 | # load the sensors we have 244 | for sensor in ws.last_values.keys(): 245 | check_and_append_sensor(sensor) 246 | 247 | if (not ecowitt_data[REG_ENTITIES][TYPE_SENSOR] 248 | and not ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR]): 249 | _LOGGER.error("No sensors found to monitor, check device config.") 250 | return False 251 | 252 | for component in ECOWITT_PLATFORMS: 253 | hass.async_create_task( 254 | hass.config_entries.async_forward_entry_setup(entry, component) 255 | ) 256 | 257 | ecowitt_data[DATA_READY] = True 258 | 259 | async def _async_ecowitt_update_cb(weather_data): 260 | """Primary update callback called from pyecowitt.""" 261 | _LOGGER.debug("Primary update callback triggered.") 262 | 263 | new_sensors = {} 264 | old_sensors = [] 265 | for component in ECOWITT_PLATFORMS: 266 | new_sensors[component] = [] 267 | 268 | if not hass.data[DOMAIN][entry.entry_id][DATA_READY]: 269 | await _first_data_rec(weather_data) 270 | return 271 | for sensor in weather_data.keys(): 272 | if sensor not in SENSOR_TYPES: 273 | if sensor not in IGNORED_SENSORS: 274 | _LOGGER.warning("Unhandled sensor type %s value %s, " 275 | + "file a PR.", sensor, weather_data[sensor]) 276 | elif (sensor not in ecowitt_data[REG_ENTITIES][TYPE_SENSOR] 277 | and sensor not in ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR] 278 | and sensor not in IGNORED_SENSORS 279 | and check_imp_metric_sensor(sensor)): 280 | _LOGGER.warning("Unregistered sensor type %s value %s received.", 281 | sensor, weather_data[sensor]) 282 | # try to register the sensor 283 | kind = check_and_append_sensor(sensor) 284 | if kind is not None: 285 | new_sensors[kind].append(sensor) 286 | # It's a sensor we know, not ignored, and of the wrong metricness 287 | elif ((sensor in ecowitt_data[REG_ENTITIES][TYPE_SENSOR] 288 | or sensor in ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR]) 289 | and sensor not in IGNORED_SENSORS 290 | and not check_imp_metric_sensor(sensor)): 291 | _LOGGER.warning("Removing sensor type %S.", sensor) 292 | old_sensors.append(sensor) 293 | 294 | # If we have old sensors, delete them. 295 | if old_sensors: 296 | await async_remove_ecowitt_entities(old_sensors, hass, ecowitt_data) 297 | 298 | # if we have new sensors, set them up. 299 | for component in ECOWITT_PLATFORMS: 300 | if new_sensors[component]: 301 | signal = f"{SIGNAL_ADD_ENTITIES}_{component}" 302 | async_dispatcher_send(hass, signal, new_sensors[component]) 303 | async_dispatcher_send(hass, DOMAIN) 304 | 305 | # this is part of the base async_setup_entry 306 | ws.register_listener(_async_ecowitt_update_cb) 307 | return True 308 | 309 | 310 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): 311 | """Unload a config entry.""" 312 | 313 | ws = hass.data[DOMAIN][entry.entry_id][DATA_ECOWITT] 314 | await ws.stop() 315 | 316 | unload_ok = all( 317 | await asyncio.gather( 318 | *[ 319 | hass.config_entries.async_forward_entry_unload(entry, component) 320 | for component in ECOWITT_PLATFORMS 321 | ] 322 | ) 323 | ) 324 | if unload_ok: 325 | hass.data[DOMAIN].pop(entry.entry_id) 326 | 327 | return unload_ok 328 | 329 | 330 | async def async_remove_ecowitt_entities(entities, hass, ecowitt_data): 331 | """Remove a sensor if needed.""" 332 | 333 | try: 334 | 335 | eventData = {} 336 | for entity in entities: 337 | name, uom, kind, device_class, icon, metric, sc = SENSOR_TYPES[entity] 338 | 339 | eventData[entity] = kind 340 | ecowitt_data[REG_ENTITIES][kind].remove(entity) 341 | 342 | async_dispatcher_send(hass, SIGNAL_REMOVE_ENTITIES, eventData) 343 | 344 | except Exception as e: 345 | _LOGGER.error(e) 346 | 347 | 348 | def async_add_ecowitt_entities(hass, entry, entity_type, 349 | platform, async_add_entities, 350 | discovery_info): 351 | entities = [] 352 | if discovery_info is None: 353 | return 354 | 355 | for new_entity in discovery_info: 356 | if new_entity not in hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][platform]: 357 | hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][platform].append(new_entity) 358 | name, uom, kind, device_class, icon, metric, sc = SENSOR_TYPES[new_entity] 359 | entities.append(entity_type(hass, entry, new_entity, name, 360 | device_class, uom, icon, sc)) 361 | if entities: 362 | async_add_entities(entities, True) 363 | 364 | 365 | class EcowittEntity(Entity): 366 | """Base class for Ecowitt Weather Station.""" 367 | 368 | def __init__(self, hass, entry, key, name): 369 | """Construct the entity.""" 370 | self.hass = hass 371 | self._key = key 372 | self._name = name 373 | self._stationinfo = hass.data[DOMAIN][entry.entry_id][DATA_STATION] 374 | self._ws = hass.data[DOMAIN][entry.entry_id][DATA_ECOWITT] 375 | self._entry = entry 376 | 377 | @property 378 | def should_poll(self): 379 | """Ecowitt is a push.""" 380 | return False 381 | 382 | @property 383 | def unique_id(self): 384 | """Return a unique ID for this sensor.""" 385 | return f"{self._stationinfo[DATA_PASSKEY]}-{self._key}" 386 | 387 | @property 388 | def name(self): 389 | """Return the name of the sensor.""" 390 | return self._name 391 | 392 | @property 393 | def device_info(self): 394 | """Return device information for this sensor.""" 395 | if (self._entry.data[CONF_NAME] != '' 396 | and self._entry.data[CONF_NAME] is not None): 397 | dname = self._entry.data[CONF_NAME] 398 | else: 399 | dname = DOMAIN 400 | 401 | return { 402 | "identifiers": {(DOMAIN, self._stationinfo[DATA_PASSKEY])}, 403 | "name": dname, 404 | "manufacturer": DOMAIN, 405 | "model": self._stationinfo[DATA_MODEL], 406 | "sw_version": self._stationinfo[DATA_STATIONTYPE], 407 | "via_device": (DOMAIN, self._stationinfo[DATA_STATIONTYPE]), 408 | # "frequency": self._stationinfo[DATA_FREQ], 409 | } 410 | 411 | async def async_added_to_hass(self): 412 | """Setup a listener for the entity.""" 413 | async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) 414 | 415 | @callback 416 | async def remove_entity(self, discovery_info=None): 417 | """Remove an entity.""" 418 | 419 | if self._key in discovery_info.keys(): 420 | 421 | registry = await async_get_entity_registry(self.hass) 422 | 423 | entity_id = registry.async_get_entity_id( 424 | discovery_info[self._key], DOMAIN, self.unique_id 425 | ) 426 | 427 | _LOGGER.debug( 428 | f"Found entity {entity_id} for key {self._key} -> Uniqueid: {self.unique_id}" 429 | ) 430 | if entity_id: 431 | registry.async_remove(entity_id) 432 | 433 | @callback 434 | def _update_callback(self) -> None: 435 | """Call from dispatcher when state changes.""" 436 | self.async_schedule_update_ha_state(force_refresh=True) 437 | 438 | @property 439 | def assumed_state(self) -> bool: 440 | """Return whether the state is based on actual reading from device.""" 441 | if (self._ws.lastupd + 5 * 60) < time.time(): 442 | return True 443 | return False 444 | -------------------------------------------------------------------------------- /custom_components/ecowitt/const.py: -------------------------------------------------------------------------------- 1 | """Constants used by ecowitt component.""" 2 | 3 | from homeassistant.const import ( 4 | CONF_UNIT_SYSTEM_METRIC, 5 | CONF_UNIT_SYSTEM_IMPERIAL, 6 | DEGREE, 7 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 8 | CONCENTRATION_PARTS_PER_MILLION, 9 | POWER_WATT, 10 | TEMP_CELSIUS, 11 | PERCENTAGE, 12 | PRESSURE_HPA, 13 | PRESSURE_INHG, 14 | LENGTH_INCHES, 15 | LENGTH_KILOMETERS, 16 | LENGTH_MILES, 17 | SPEED_KILOMETERS_PER_HOUR, 18 | SPEED_MILES_PER_HOUR, 19 | SPEED_METERS_PER_SECOND, 20 | TIME_HOURS, 21 | TIME_DAYS, 22 | TIME_WEEKS, 23 | TIME_MONTHS, 24 | TIME_YEARS, 25 | UV_INDEX, 26 | DEVICE_CLASS_BATTERY, 27 | DEVICE_CLASS_HUMIDITY, 28 | DEVICE_CLASS_ILLUMINANCE, 29 | DEVICE_CLASS_TEMPERATURE, 30 | DEVICE_CLASS_PRESSURE, 31 | DEVICE_CLASS_TIMESTAMP, 32 | DEVICE_CLASS_VOLTAGE, 33 | ELECTRIC_POTENTIAL_VOLT, 34 | ) 35 | 36 | from homeassistant.components.binary_sensor import ( 37 | DEVICE_CLASS_MOISTURE, 38 | ) 39 | 40 | from homeassistant.components.sensor import ( 41 | STATE_CLASS_MEASUREMENT, 42 | STATE_CLASS_TOTAL_INCREASING, 43 | ) 44 | 45 | ECOWITT_PLATFORMS = ["sensor", "binary_sensor"] 46 | 47 | TYPE_SENSOR = "sensor" 48 | TYPE_BINARY_SENSOR = "binary_sensor" 49 | DOMAIN = "ecowitt" 50 | DATA_CONFIG = "config" 51 | DATA_OPTIONS = "options" 52 | DATA_ECOWITT = "ecowitt_listener" 53 | DATA_STATION = "station" 54 | DATA_PASSKEY = "PASSKEY" 55 | DATA_STATIONTYPE = "stationtype" 56 | DATA_FREQ = "freq" 57 | DATA_MODEL = "model" 58 | DATA_READY = "ready" 59 | REG_ENTITIES = "registered" 60 | 61 | DEFAULT_PORT = 4199 62 | 63 | SIGNAL_ADD_ENTITIES = "ecowitt_add_entities" 64 | SIGNAL_REMOVE_ENTITIES = "ecowitt_remove_entities" 65 | 66 | CONF_NAME = "component_name" 67 | CONF_UNIT_BARO = "barounit" 68 | CONF_UNIT_WIND = "windunit" 69 | CONF_UNIT_RAIN = "rainunit" 70 | CONF_UNIT_WINDCHILL = "windchillunit" 71 | CONF_UNIT_LIGHTNING = "lightningunit" 72 | 73 | TYPE_BAROMABSHPA = "baromabshpa" 74 | TYPE_BAROMRELHPA = "baromrelhpa" 75 | TYPE_BAROMABSIN = "baromabsin" 76 | TYPE_BAROMRELIN = "baromrelin" 77 | TYPE_RAINRATEIN = "rainratein" 78 | TYPE_EVENTRAININ = "eventrainin" 79 | TYPE_HOURLYRAININ = "hourlyrainin" 80 | TYPE_TOTALRAININ = "totalrainin" 81 | TYPE_DAILYRAININ = "dailyrainin" 82 | TYPE_WEEKLYRAININ = "weeklyrainin" 83 | TYPE_MONTHLYRAININ = "monthlyrainin" 84 | TYPE_YEARLYRAININ = "yearlyrainin" 85 | TYPE_RAINRATEMM = "rainratemm" 86 | TYPE_EVENTRAINMM = "eventrainmm" 87 | TYPE_HOURLYRAINMM = "hourlyrainmm" 88 | TYPE_TOTALRAINMM = "totalrainmm" 89 | TYPE_DAILYRAINMM = "dailyrainmm" 90 | TYPE_WEEKLYRAINMM = "weeklyrainmm" 91 | TYPE_MONTHLYRAINMM = "monthlyrainmm" 92 | TYPE_YEARLYRAINMM = "yearlyrainmm" 93 | TYPE_HUMIDITY = "humidity" 94 | TYPE_HUMIDITY1 = "humidity1" 95 | TYPE_HUMIDITY2 = "humidity2" 96 | TYPE_HUMIDITY3 = "humidity3" 97 | TYPE_HUMIDITY4 = "humidity4" 98 | TYPE_HUMIDITY5 = "humidity5" 99 | TYPE_HUMIDITY6 = "humidity6" 100 | TYPE_HUMIDITY7 = "humidity7" 101 | TYPE_HUMIDITY8 = "humidity8" 102 | TYPE_HUMIDITYIN = "humidityin" 103 | TYPE_WINDDIR = "winddir" 104 | TYPE_WINDDIR_A10 = "winddir_avg10m" 105 | TYPE_WINDSPEEDKMH = "windspeedkmh" 106 | TYPE_WINDSPEEDKMH_A10 = "windspdkmh_avg10m" 107 | TYPE_WINDGUSTKMH = "windgustkmh" 108 | TYPE_WINDSPEEDMPH = "windspeedmph" 109 | TYPE_WINDSPEEDMPH_A10 = "windspdmph_avg10m" 110 | TYPE_WINDGUSTMPH = "windgustmph" 111 | TYPE_MAXDAILYGUST = "maxdailygust" 112 | TYPE_MAXDAILYGUSTKMH = "maxdailygustkmh" 113 | TYPE_WINDGUSTMS = "windgustms" 114 | TYPE_WINDSPEEDMS = "windspeedms" 115 | TYPE_WINDSPEEDMS_A10 = "windspdms_avg10m" 116 | TYPE_MAXDAILYGUSTMS = "maxdailygustms" 117 | TYPE_TEMPC = "tempc" 118 | TYPE_TEMPINC = "tempinc" 119 | TYPE_TEMP1C = "temp1c" 120 | TYPE_TEMP2C = "temp2c" 121 | TYPE_TEMP3C = "temp3c" 122 | TYPE_TEMP4C = "temp4c" 123 | TYPE_TEMP5C = "temp5c" 124 | TYPE_TEMP6C = "temp6c" 125 | TYPE_TEMP7C = "temp7c" 126 | TYPE_TEMP8C = "temp8c" 127 | TYPE_DEWPOINTC = "dewpointc" 128 | TYPE_DEWPOINTINC = "dewpointinc" 129 | TYPE_DEWPOINT1C = "dewpoint1c" 130 | TYPE_DEWPOINT2C = "dewpoint2c" 131 | TYPE_DEWPOINT3C = "dewpoint3c" 132 | TYPE_DEWPOINT4C = "dewpoint4c" 133 | TYPE_DEWPOINT5C = "dewpoint5c" 134 | TYPE_DEWPOINT6C = "dewpoint6c" 135 | TYPE_DEWPOINT7C = "dewpoint7c" 136 | TYPE_DEWPOINT8C = "dewpoint8c" 137 | TYPE_WINDCHILLC = "windchillc" 138 | TYPE_SOLARRADIATION = "solarradiation" 139 | TYPE_UV = "uv" 140 | TYPE_SOILMOISTURE1 = "soilmoisture1" 141 | TYPE_SOILMOISTURE2 = "soilmoisture2" 142 | TYPE_SOILMOISTURE3 = "soilmoisture3" 143 | TYPE_SOILMOISTURE4 = "soilmoisture4" 144 | TYPE_SOILMOISTURE5 = "soilmoisture5" 145 | TYPE_SOILMOISTURE6 = "soilmoisture6" 146 | TYPE_SOILMOISTURE7 = "soilmoisture7" 147 | TYPE_SOILMOISTURE8 = "soilmoisture8" 148 | TYPE_PM25_CH1 = "pm25_ch1" 149 | TYPE_PM25_CH2 = "pm25_ch2" 150 | TYPE_PM25_CH3 = "pm25_ch3" 151 | TYPE_PM25_CH4 = "pm25_ch4" 152 | TYPE_PM25_AVG_24H_CH1 = "pm25_avg_24h_ch1" 153 | TYPE_PM25_AVG_24H_CH2 = "pm25_avg_24h_ch2" 154 | TYPE_PM25_AVG_24H_CH3 = "pm25_avg_24h_ch3" 155 | TYPE_PM25_AVG_24H_CH4 = "pm25_avg_24h_ch4" 156 | TYPE_LIGHTNING_TIME = "lightning_time" 157 | TYPE_LIGHTNING_NUM = "lightning_num" 158 | TYPE_LIGHTNING_KM = "lightning" 159 | TYPE_LIGHTNING_MI = "lightning_mi" 160 | TYPE_CO2_TEMP = "tf_co2" 161 | TYPE_CO2_TEMPC = "tf_co2c" 162 | TYPE_CO2_HUMIDITY = "humi_co2" 163 | TYPE_CO2_PM25 = "pm25_co2" 164 | TYPE_CO2_PM25_AVG_24H = "pm25_24h_co2" 165 | TYPE_CO2_PM10 = "pm10_co2" 166 | TYPE_CO2_PM10_AVG_24H = "pm10_24h_co2" 167 | TYPE_CO2_CO2 = "co2" 168 | TYPE_CO2_CO2_AVG_24H = "co2_24h" 169 | TYPE_CO2_BATT = "co2_batt" 170 | TYPE_LEAK_CH1 = "leak_ch1" 171 | TYPE_LEAK_CH2 = "leak_ch2" 172 | TYPE_LEAK_CH3 = "leak_ch3" 173 | TYPE_LEAK_CH4 = "leak_ch4" 174 | TYPE_WH25BATT = "wh25batt" 175 | TYPE_WH26BATT = "wh26batt" 176 | TYPE_WH40BATT = "wh40batt" 177 | TYPE_WH57BATT = "wh57batt" 178 | TYPE_WH68BATT = "wh68batt" 179 | TYPE_WH65BATT = "wh65batt" 180 | TYPE_WH80BATT = "wh80batt" 181 | TYPE_SOILBATT1 = "soilbatt1" 182 | TYPE_SOILBATT2 = "soilbatt2" 183 | TYPE_SOILBATT3 = "soilbatt3" 184 | TYPE_SOILBATT4 = "soilbatt4" 185 | TYPE_SOILBATT5 = "soilbatt5" 186 | TYPE_SOILBATT6 = "soilbatt6" 187 | TYPE_SOILBATT7 = "soilbatt7" 188 | TYPE_SOILBATT8 = "soilbatt8" 189 | TYPE_BATTERY1 = "batt1" 190 | TYPE_BATTERY2 = "batt2" 191 | TYPE_BATTERY3 = "batt3" 192 | TYPE_BATTERY4 = "batt4" 193 | TYPE_BATTERY5 = "batt5" 194 | TYPE_BATTERY6 = "batt6" 195 | TYPE_BATTERY7 = "batt7" 196 | TYPE_BATTERY8 = "batt8" 197 | TYPE_PM25BATT1 = "pm25batt1" 198 | TYPE_PM25BATT2 = "pm25batt2" 199 | TYPE_PM25BATT3 = "pm25batt3" 200 | TYPE_PM25BATT4 = "pm25batt4" 201 | TYPE_PM25BATT5 = "pm25batt5" 202 | TYPE_PM25BATT6 = "pm25batt6" 203 | TYPE_PM25BATT7 = "pm25batt7" 204 | TYPE_PM25BATT8 = "pm25batt8" 205 | TYPE_LEAKBATT1 = "leakbatt1" 206 | TYPE_LEAKBATT2 = "leakbatt2" 207 | TYPE_LEAKBATT3 = "leakbatt3" 208 | TYPE_LEAKBATT4 = "leakbatt4" 209 | TYPE_LEAKBATT5 = "leakbatt5" 210 | TYPE_LEAKBATT6 = "leakbatt6" 211 | TYPE_LEAKBATT7 = "leakbatt7" 212 | TYPE_LEAKBATT8 = "leakbatt8" 213 | TYPE_WN34TEMP1C = "tf_ch1c" 214 | TYPE_WN34TEMP2C = "tf_ch2c" 215 | TYPE_WN34TEMP3C = "tf_ch3c" 216 | TYPE_WN34TEMP4C = "tf_ch4c" 217 | TYPE_WN34TEMP5C = "tf_ch5c" 218 | TYPE_WN34TEMP6C = "tf_ch6c" 219 | TYPE_WN34TEMP7C = "tf_ch7c" 220 | TYPE_WN34TEMP8C = "tf_ch8c" 221 | TYPE_WN34BATT1 = "tf_batt1" 222 | TYPE_WN34BATT2 = "tf_batt2" 223 | TYPE_WN34BATT3 = "tf_batt3" 224 | TYPE_WN34BATT4 = "tf_batt4" 225 | TYPE_WN34BATT5 = "tf_batt5" 226 | TYPE_WN34BATT6 = "tf_batt6" 227 | TYPE_WN34BATT7 = "tf_batt7" 228 | TYPE_WN34BATT8 = "tf_batt8" 229 | 230 | S_METRIC = 1 231 | S_IMPERIAL = 2 232 | S_METRIC_MS = 3 233 | 234 | W_TYPE_NEW = "new" 235 | W_TYPE_OLD = "old" 236 | W_TYPE_HYBRID = "hybrid" 237 | CONF_UNIT_SYSTEM_METRIC_MS = "metric_ms" 238 | 239 | LEAK_DETECTED = "Leak Detected" 240 | 241 | UNIT_OPTS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] 242 | WIND_OPTS = [ 243 | CONF_UNIT_SYSTEM_METRIC, 244 | CONF_UNIT_SYSTEM_IMPERIAL, 245 | CONF_UNIT_SYSTEM_METRIC_MS 246 | ] 247 | WINDCHILL_OPTS = [W_TYPE_HYBRID, W_TYPE_NEW, W_TYPE_OLD] 248 | 249 | 250 | # Name, unit_of_measure, type, device_class, icon, metric=1, state_class 251 | # name, uom, kind, device_class, icon, metric, state_class = SENSOR_TYPES[x] 252 | SENSOR_TYPES = { 253 | TYPE_BAROMABSHPA: ("Absolute Pressure", PRESSURE_HPA, 254 | TYPE_SENSOR, DEVICE_CLASS_PRESSURE, 255 | "mdi:gauge", S_METRIC, STATE_CLASS_MEASUREMENT), 256 | TYPE_BAROMRELHPA: ("Relative Pressure", PRESSURE_HPA, 257 | TYPE_SENSOR, DEVICE_CLASS_PRESSURE, 258 | "mdi:gauge", S_METRIC, STATE_CLASS_MEASUREMENT), 259 | TYPE_BAROMABSIN: ("Absolute Pressure", PRESSURE_INHG, 260 | TYPE_SENSOR, DEVICE_CLASS_PRESSURE, 261 | "mdi:gauge", S_IMPERIAL, STATE_CLASS_MEASUREMENT), 262 | TYPE_BAROMRELIN: ("Relative Pressure", PRESSURE_INHG, 263 | TYPE_SENSOR, DEVICE_CLASS_PRESSURE, 264 | "mdi:gauge", S_IMPERIAL, STATE_CLASS_MEASUREMENT), 265 | TYPE_RAINRATEIN: ("Rain Rate", f"{LENGTH_INCHES}/{TIME_HOURS}", 266 | TYPE_SENSOR, None, "mdi:water", S_IMPERIAL, 267 | STATE_CLASS_TOTAL_INCREASING), 268 | TYPE_EVENTRAININ: ("Event Rain Rate", f"{LENGTH_INCHES}/{TIME_HOURS}", 269 | TYPE_SENSOR, None, "mdi:water", S_IMPERIAL, 270 | STATE_CLASS_TOTAL_INCREASING), 271 | TYPE_HOURLYRAININ: ("Hourly Rain Rate", f"{LENGTH_INCHES}/{TIME_HOURS}", 272 | TYPE_SENSOR, None, "mdi:water", S_IMPERIAL, 273 | STATE_CLASS_TOTAL_INCREASING), 274 | TYPE_TOTALRAININ: ("Total Rain Rate", f"{LENGTH_INCHES}", 275 | TYPE_SENSOR, None, "mdi:water", S_IMPERIAL, 276 | STATE_CLASS_TOTAL_INCREASING), 277 | TYPE_DAILYRAININ: ("Daily Rain Rate", f"{LENGTH_INCHES}/{TIME_DAYS}", 278 | TYPE_SENSOR, None, "mdi:water", S_IMPERIAL, 279 | STATE_CLASS_TOTAL_INCREASING), 280 | TYPE_WEEKLYRAININ: ("Weekly Rain Rate", f"{LENGTH_INCHES}/{TIME_WEEKS}", 281 | TYPE_SENSOR, None, "mdi:water", S_IMPERIAL, 282 | STATE_CLASS_TOTAL_INCREASING), 283 | TYPE_MONTHLYRAININ: ("Monthly Rain Rate", f"{LENGTH_INCHES}/{TIME_MONTHS}", 284 | TYPE_SENSOR, None, "mdi:water", S_IMPERIAL, 285 | STATE_CLASS_TOTAL_INCREASING), 286 | TYPE_YEARLYRAININ: ("Yearly Rain Rate", f"{LENGTH_INCHES}/{TIME_YEARS}", 287 | TYPE_SENSOR, None, "mdi:water", S_IMPERIAL, 288 | STATE_CLASS_TOTAL_INCREASING), 289 | TYPE_RAINRATEMM: ("Rain Rate", f"mm/{TIME_HOURS}", 290 | TYPE_SENSOR, None, "mdi:water", S_METRIC, 291 | STATE_CLASS_TOTAL_INCREASING), 292 | TYPE_EVENTRAINMM: ("Event Rain Rate", f"mm/{TIME_HOURS}", 293 | TYPE_SENSOR, None, "mdi:water", S_METRIC, 294 | STATE_CLASS_TOTAL_INCREASING), 295 | TYPE_HOURLYRAINMM: ("Hourly Rain Rate", f"mm/{TIME_HOURS}", 296 | TYPE_SENSOR, None, "mdi:water", S_METRIC, 297 | STATE_CLASS_TOTAL_INCREASING), 298 | TYPE_TOTALRAINMM: ("Total Rain Rate", f"mm", 299 | TYPE_SENSOR, None, "mdi:water", S_METRIC, 300 | STATE_CLASS_TOTAL_INCREASING), 301 | TYPE_DAILYRAINMM: ("Daily Rain Rate", f"mm/{TIME_DAYS}", 302 | TYPE_SENSOR, None, "mdi:water", S_METRIC, 303 | STATE_CLASS_TOTAL_INCREASING), 304 | TYPE_WEEKLYRAINMM: ("Weekly Rain Rate", f"mm/{TIME_WEEKS}", 305 | TYPE_SENSOR, None, "mdi:water", S_METRIC, 306 | STATE_CLASS_TOTAL_INCREASING), 307 | TYPE_MONTHLYRAINMM: ("Monthly Rain Rate", f"mm/{TIME_MONTHS}", 308 | TYPE_SENSOR, None, "mdi:water", S_METRIC, 309 | STATE_CLASS_TOTAL_INCREASING), 310 | TYPE_YEARLYRAINMM: ("Yearly Rain Rate", f"mm/{TIME_YEARS}", 311 | TYPE_SENSOR, None, "mdi:water", S_METRIC, 312 | STATE_CLASS_TOTAL_INCREASING), 313 | TYPE_HUMIDITY: ("Humidity", PERCENTAGE, 314 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 315 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 316 | TYPE_HUMIDITYIN: ("Indoor Humidity", PERCENTAGE, 317 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 318 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 319 | TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, 320 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 321 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 322 | TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, 323 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 324 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 325 | TYPE_HUMIDITY3: ("Humidity 3", PERCENTAGE, 326 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 327 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 328 | TYPE_HUMIDITY4: ("Humidity 4", PERCENTAGE, 329 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 330 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 331 | TYPE_HUMIDITY5: ("Humidity 5", PERCENTAGE, 332 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 333 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 334 | TYPE_HUMIDITY6: ("Humidity 6", PERCENTAGE, 335 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 336 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 337 | TYPE_HUMIDITY7: ("Humidity 7", PERCENTAGE, 338 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 339 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 340 | TYPE_HUMIDITY8: ("Humidity 8", PERCENTAGE, 341 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 342 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 343 | TYPE_WINDDIR: ("Wind Direction", DEGREE, 344 | TYPE_SENSOR, None, "mdi:compass", 0, STATE_CLASS_MEASUREMENT), 345 | TYPE_WINDDIR_A10: ("Wind Direction 10m Avg", DEGREE, 346 | TYPE_SENSOR, None, "mdi:compass", 0, 347 | STATE_CLASS_MEASUREMENT), 348 | TYPE_WINDSPEEDKMH: ("Wind Speed", SPEED_KILOMETERS_PER_HOUR, 349 | TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC, 350 | STATE_CLASS_MEASUREMENT), 351 | TYPE_WINDSPEEDKMH_A10: ("Wind Speed 10m Avg", SPEED_KILOMETERS_PER_HOUR, 352 | TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC, 353 | STATE_CLASS_MEASUREMENT), 354 | TYPE_WINDGUSTKMH: ("Wind Gust", SPEED_KILOMETERS_PER_HOUR, 355 | TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC, 356 | STATE_CLASS_MEASUREMENT), 357 | TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, 358 | TYPE_SENSOR, None, "mdi:weather-windy", S_IMPERIAL, 359 | STATE_CLASS_MEASUREMENT), 360 | TYPE_WINDSPEEDMPH_A10: ("Wind Speed 10m Avg", SPEED_MILES_PER_HOUR, 361 | TYPE_SENSOR, None, "mdi:weather-windy", 362 | S_IMPERIAL, STATE_CLASS_MEASUREMENT), 363 | TYPE_WINDGUSTMPH: ("Wind Gust", SPEED_MILES_PER_HOUR, 364 | TYPE_SENSOR, None, "mdi:weather-windy", S_IMPERIAL, 365 | STATE_CLASS_MEASUREMENT), 366 | TYPE_MAXDAILYGUST: ("Max Daily Wind Gust", SPEED_MILES_PER_HOUR, 367 | TYPE_SENSOR, None, "mdi:weather-windy", S_IMPERIAL, 368 | STATE_CLASS_MEASUREMENT), 369 | TYPE_MAXDAILYGUSTKMH: ("Max Daily Wind Gust", SPEED_KILOMETERS_PER_HOUR, 370 | TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC, 371 | STATE_CLASS_MEASUREMENT), 372 | TYPE_WINDGUSTMS: ("Wind Gust", SPEED_METERS_PER_SECOND, 373 | TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC_MS, 374 | STATE_CLASS_MEASUREMENT), 375 | TYPE_WINDSPEEDMS: ("Wind Speed", SPEED_METERS_PER_SECOND, 376 | TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC_MS, 377 | STATE_CLASS_MEASUREMENT), 378 | TYPE_WINDSPEEDMS_A10: ("Wind Speed", SPEED_METERS_PER_SECOND, 379 | TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC_MS, 380 | STATE_CLASS_MEASUREMENT), 381 | TYPE_MAXDAILYGUSTMS: ("Max Daily Wind Gust", SPEED_METERS_PER_SECOND, 382 | TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC_MS, 383 | STATE_CLASS_MEASUREMENT), 384 | TYPE_TEMPC: ("Outdoor Temperature", TEMP_CELSIUS, 385 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0, 386 | STATE_CLASS_MEASUREMENT), 387 | TYPE_TEMP1C: ("Temperature 1", TEMP_CELSIUS, 388 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0, 389 | STATE_CLASS_MEASUREMENT), 390 | TYPE_TEMP2C: ("Temperature 2", TEMP_CELSIUS, 391 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0, 392 | STATE_CLASS_MEASUREMENT), 393 | TYPE_TEMP3C: ("Temperature 3", TEMP_CELSIUS, 394 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0, 395 | STATE_CLASS_MEASUREMENT), 396 | TYPE_TEMP4C: ("Temperature 4", TEMP_CELSIUS, 397 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0, 398 | STATE_CLASS_MEASUREMENT), 399 | TYPE_TEMP5C: ("Temperature 5", TEMP_CELSIUS, 400 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0, 401 | STATE_CLASS_MEASUREMENT), 402 | TYPE_TEMP6C: ("Temperature 6", TEMP_CELSIUS, 403 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0, 404 | STATE_CLASS_MEASUREMENT), 405 | TYPE_TEMP7C: ("Temperature 7", TEMP_CELSIUS, 406 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0, 407 | STATE_CLASS_MEASUREMENT), 408 | TYPE_TEMP8C: ("Temperature 8", TEMP_CELSIUS, 409 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0, 410 | STATE_CLASS_MEASUREMENT), 411 | TYPE_TEMPINC: ("Indoor Temperature", TEMP_CELSIUS, 412 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 413 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 414 | TYPE_DEWPOINTC: ("Dewpoint", TEMP_CELSIUS, 415 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 416 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 417 | TYPE_DEWPOINTINC: ("Indoor Dewpoint", TEMP_CELSIUS, 418 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 419 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 420 | TYPE_DEWPOINT1C: ("Dewpoint 1", TEMP_CELSIUS, 421 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 422 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 423 | TYPE_DEWPOINT2C: ("Dewpoint 2", TEMP_CELSIUS, 424 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 425 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 426 | TYPE_DEWPOINT3C: ("Dewpoint 3", TEMP_CELSIUS, 427 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 428 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 429 | TYPE_DEWPOINT4C: ("Dewpoint 4", TEMP_CELSIUS, 430 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 431 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 432 | TYPE_DEWPOINT5C: ("Dewpoint 5", TEMP_CELSIUS, 433 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 434 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 435 | TYPE_DEWPOINT6C: ("Dewpoint 6", TEMP_CELSIUS, 436 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 437 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 438 | TYPE_DEWPOINT7C: ("Dewpoint 7", TEMP_CELSIUS, 439 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 440 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 441 | TYPE_DEWPOINT8C: ("Dewpoint 8", TEMP_CELSIUS, 442 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 443 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 444 | TYPE_WINDCHILLC: ("Windchill", TEMP_CELSIUS, 445 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 446 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 447 | TYPE_SOLARRADIATION: ("Solar Radiation", f"{POWER_WATT}/m^2", 448 | TYPE_SENSOR, DEVICE_CLASS_ILLUMINANCE, 449 | "mdi:weather-sunny", 0, STATE_CLASS_MEASUREMENT), 450 | TYPE_UV: ("UV Index", UV_INDEX, 451 | TYPE_SENSOR, None, "mdi:sunglasses", 0, STATE_CLASS_MEASUREMENT), 452 | TYPE_SOILMOISTURE1: ("Soil Moisture 1", PERCENTAGE, 453 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 454 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 455 | TYPE_SOILMOISTURE2: ("Soil Moisture 2", PERCENTAGE, 456 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 457 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 458 | TYPE_SOILMOISTURE3: ("Soil Moisture 3", PERCENTAGE, 459 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 460 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 461 | TYPE_SOILMOISTURE4: ("Soil Moisture 4", PERCENTAGE, 462 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 463 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 464 | TYPE_SOILMOISTURE5: ("Soil Moisture 5", PERCENTAGE, 465 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 466 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 467 | TYPE_SOILMOISTURE6: ("Soil Moisture 6", PERCENTAGE, 468 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 469 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 470 | TYPE_SOILMOISTURE7: ("Soil Moisture 7", PERCENTAGE, 471 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 472 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 473 | TYPE_SOILMOISTURE8: ("Soil Moisture 8", PERCENTAGE, 474 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 475 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 476 | TYPE_PM25_CH1: ("PM2.5 1", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 477 | TYPE_SENSOR, None, "mdi:eye", 0, STATE_CLASS_MEASUREMENT), 478 | TYPE_PM25_CH2: ("PM2.5 2", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 479 | TYPE_SENSOR, None, "mdi:eye", 0, STATE_CLASS_MEASUREMENT), 480 | TYPE_PM25_CH3: ("PM2.5 3", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 481 | TYPE_SENSOR, None, "mdi:eye", 0, STATE_CLASS_MEASUREMENT), 482 | TYPE_PM25_CH4: ("PM2.5 4", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 483 | TYPE_SENSOR, None, "mdi:eye", 0, STATE_CLASS_MEASUREMENT), 484 | TYPE_PM25_AVG_24H_CH1: ("PM2.5 24h average 1", 485 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 486 | TYPE_SENSOR, None, "mdi:eye", 0, 487 | STATE_CLASS_MEASUREMENT), 488 | TYPE_PM25_AVG_24H_CH2: ("PM2.5 24h average 2", 489 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 490 | TYPE_SENSOR, None, "mdi:eye", 0, 491 | STATE_CLASS_MEASUREMENT), 492 | TYPE_PM25_AVG_24H_CH3: ("PM2.5 24h average 3", 493 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 494 | TYPE_SENSOR, None, "mdi:eye", 0, 495 | STATE_CLASS_MEASUREMENT), 496 | TYPE_PM25_AVG_24H_CH4: ("PM2.5 24h average 4", 497 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 498 | TYPE_SENSOR, None, "mdi:eye", 0, 499 | STATE_CLASS_MEASUREMENT), 500 | TYPE_LIGHTNING_TIME: ("Last Lightning strike", "", 501 | TYPE_SENSOR, DEVICE_CLASS_TIMESTAMP, "mdi:clock", 0, 502 | STATE_CLASS_MEASUREMENT), 503 | TYPE_LIGHTNING_NUM: ("Lightning strikes", f"strikes/{TIME_DAYS}", 504 | TYPE_SENSOR, None, "mdi:weather-lightning", 0, 505 | STATE_CLASS_TOTAL_INCREASING), 506 | TYPE_LIGHTNING_KM: ("Lightning strike distance", LENGTH_KILOMETERS, 507 | TYPE_SENSOR, None, "mdi:ruler", S_METRIC, 508 | STATE_CLASS_MEASUREMENT), 509 | TYPE_LIGHTNING_MI: ("Lightning strike distance", LENGTH_MILES, 510 | TYPE_SENSOR, None, "mdi:ruler", S_IMPERIAL, 511 | STATE_CLASS_MEASUREMENT), 512 | TYPE_LEAK_CH1: ("Leak Detection 1", LEAK_DETECTED, TYPE_BINARY_SENSOR, 513 | DEVICE_CLASS_MOISTURE, "mdi:leak", 0, None), 514 | TYPE_LEAK_CH2: ("Leak Detection 2", LEAK_DETECTED, TYPE_BINARY_SENSOR, 515 | DEVICE_CLASS_MOISTURE, "mdi:leak", 0, None), 516 | TYPE_LEAK_CH3: ("Leak Detection 3", LEAK_DETECTED, TYPE_BINARY_SENSOR, 517 | DEVICE_CLASS_MOISTURE, "mdi:leak", 0, None), 518 | TYPE_LEAK_CH4: ("Leak Detection 4", LEAK_DETECTED, TYPE_BINARY_SENSOR, 519 | DEVICE_CLASS_MOISTURE, "mdi:leak", 0, None), 520 | TYPE_CO2_PM25: ("WH45 PM2.5", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 521 | TYPE_SENSOR, None, "mdi:eye", 0, STATE_CLASS_MEASUREMENT), 522 | TYPE_CO2_PM25_AVG_24H: ("WH45 PM2.5 24h average", 523 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 524 | TYPE_SENSOR, None, "mdi:eye", 0, 525 | STATE_CLASS_MEASUREMENT), 526 | TYPE_CO2_PM10: ("WH45 PM10", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 527 | TYPE_SENSOR, None, "mdi:eye", 0, STATE_CLASS_MEASUREMENT), 528 | TYPE_CO2_PM10_AVG_24H: ("WH45 PM10 24h average", 529 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 530 | TYPE_SENSOR, None, "mdi:eye", 0, 531 | STATE_CLASS_MEASUREMENT), 532 | TYPE_CO2_HUMIDITY: ("WH45 Humidity", PERCENTAGE, 533 | TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, 534 | "mdi:water-percent", 0, STATE_CLASS_MEASUREMENT), 535 | TYPE_CO2_TEMPC: ("WH45 Temperature", TEMP_CELSIUS, 536 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 537 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 538 | TYPE_CO2_CO2: ("WH45 CO2", CONCENTRATION_PARTS_PER_MILLION, 539 | TYPE_SENSOR, None, "mdi:molecule-co2", 0, 540 | STATE_CLASS_MEASUREMENT), 541 | TYPE_CO2_CO2_AVG_24H: ("WH45 CO2 24h average", CONCENTRATION_PARTS_PER_MILLION, 542 | TYPE_SENSOR, None, "mdi:molecule-co2", 0, 543 | STATE_CLASS_MEASUREMENT), 544 | TYPE_CO2_BATT: ("WH45 Battery", PERCENTAGE, TYPE_SENSOR, 545 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 546 | STATE_CLASS_MEASUREMENT), 547 | TYPE_WH25BATT: ("WH25 Battery", "BATT", TYPE_BINARY_SENSOR, 548 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 549 | TYPE_WH26BATT: ("WH26 Battery", "BATT", TYPE_BINARY_SENSOR, 550 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 551 | TYPE_WH40BATT: ("WH40 Battery", ELECTRIC_POTENTIAL_VOLT, TYPE_SENSOR, 552 | DEVICE_CLASS_VOLTAGE, "mdi:battery", 0, 553 | STATE_CLASS_MEASUREMENT), 554 | TYPE_WH57BATT: ("WH57 Battery", PERCENTAGE, TYPE_SENSOR, 555 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 556 | STATE_CLASS_MEASUREMENT), 557 | TYPE_WH65BATT: ("WH65 Battery", "BATT", TYPE_BINARY_SENSOR, 558 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 559 | TYPE_WH68BATT: ("WH68 Battery", ELECTRIC_POTENTIAL_VOLT, TYPE_SENSOR, 560 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 561 | STATE_CLASS_MEASUREMENT), 562 | TYPE_WH80BATT: ("WH80 Battery", ELECTRIC_POTENTIAL_VOLT, TYPE_SENSOR, 563 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 564 | STATE_CLASS_MEASUREMENT), 565 | TYPE_SOILBATT1: ("Soil Moisture 1 Battery", ELECTRIC_POTENTIAL_VOLT, 566 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, "mdi:battery", 0, 567 | STATE_CLASS_MEASUREMENT), 568 | TYPE_SOILBATT2: ("Soil Moisture 2 Battery", ELECTRIC_POTENTIAL_VOLT, 569 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, "mdi:battery", 0, 570 | STATE_CLASS_MEASUREMENT), 571 | TYPE_SOILBATT3: ("Soil Moisture 3 Battery", ELECTRIC_POTENTIAL_VOLT, 572 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, "mdi:battery", 0, 573 | STATE_CLASS_MEASUREMENT), 574 | TYPE_SOILBATT4: ("Soil Moisture 4 Battery", ELECTRIC_POTENTIAL_VOLT, 575 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, "mdi:battery", 0, 576 | STATE_CLASS_MEASUREMENT), 577 | TYPE_SOILBATT5: ("Soil Moisture 5 Battery", ELECTRIC_POTENTIAL_VOLT, 578 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, "mdi:battery", 0, 579 | STATE_CLASS_MEASUREMENT), 580 | TYPE_SOILBATT6: ("Soil Moisture 6 Battery", ELECTRIC_POTENTIAL_VOLT, 581 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, "mdi:battery", 0, 582 | STATE_CLASS_MEASUREMENT), 583 | TYPE_SOILBATT7: ("Soil Moisture 7 Battery", ELECTRIC_POTENTIAL_VOLT, 584 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, "mdi:battery", 0, 585 | STATE_CLASS_MEASUREMENT), 586 | TYPE_SOILBATT8: ("Soil Moisture 8 Battery", ELECTRIC_POTENTIAL_VOLT, 587 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, "mdi:battery", 0, 588 | STATE_CLASS_MEASUREMENT), 589 | TYPE_BATTERY1: ("Battery 1", "BATT", TYPE_BINARY_SENSOR, 590 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 591 | TYPE_BATTERY2: ("Battery 2", "BATT", TYPE_BINARY_SENSOR, 592 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 593 | TYPE_BATTERY3: ("Battery 3", "BATT", TYPE_BINARY_SENSOR, 594 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 595 | TYPE_BATTERY4: ("Battery 4", "BATT", TYPE_BINARY_SENSOR, 596 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 597 | TYPE_BATTERY5: ("Battery 5", "BATT", TYPE_BINARY_SENSOR, 598 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 599 | TYPE_BATTERY6: ("Battery 6", "BATT", TYPE_BINARY_SENSOR, 600 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 601 | TYPE_BATTERY7: ("Battery 7", "BATT", TYPE_BINARY_SENSOR, 602 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 603 | TYPE_BATTERY8: ("Battery 8", "BATT", TYPE_BINARY_SENSOR, 604 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, None), 605 | TYPE_PM25BATT1: ("PM2.5 1 Battery", PERCENTAGE, TYPE_SENSOR, 606 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 607 | STATE_CLASS_MEASUREMENT), 608 | TYPE_PM25BATT2: ("PM2.5 2 Battery", PERCENTAGE, TYPE_SENSOR, 609 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 610 | STATE_CLASS_MEASUREMENT), 611 | TYPE_PM25BATT3: ("PM2.5 3 Battery", PERCENTAGE, TYPE_SENSOR, 612 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 613 | STATE_CLASS_MEASUREMENT), 614 | TYPE_PM25BATT4: ("PM2.5 4 Battery", PERCENTAGE, TYPE_SENSOR, 615 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 616 | STATE_CLASS_MEASUREMENT), 617 | TYPE_PM25BATT5: ("PM2.5 5 Battery", PERCENTAGE, TYPE_SENSOR, 618 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 619 | STATE_CLASS_MEASUREMENT), 620 | TYPE_PM25BATT6: ("PM2.5 6 Battery", PERCENTAGE, TYPE_SENSOR, 621 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 622 | STATE_CLASS_MEASUREMENT), 623 | TYPE_PM25BATT7: ("PM2.5 7 Battery", PERCENTAGE, TYPE_SENSOR, 624 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 625 | STATE_CLASS_MEASUREMENT), 626 | TYPE_PM25BATT8: ("PM2.5 8 Battery", PERCENTAGE, TYPE_SENSOR, 627 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 628 | STATE_CLASS_MEASUREMENT), 629 | TYPE_LEAKBATT1: ("Leak 1 Battery", PERCENTAGE, TYPE_SENSOR, 630 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 631 | STATE_CLASS_MEASUREMENT), 632 | TYPE_LEAKBATT2: ("Leak 2 Battery", PERCENTAGE, TYPE_SENSOR, 633 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 634 | STATE_CLASS_MEASUREMENT), 635 | TYPE_LEAKBATT3: ("Leak 3 Battery", PERCENTAGE, TYPE_SENSOR, 636 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 637 | STATE_CLASS_MEASUREMENT), 638 | TYPE_LEAKBATT4: ("Leak 4 Battery", PERCENTAGE, TYPE_SENSOR, 639 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 640 | STATE_CLASS_MEASUREMENT), 641 | TYPE_LEAKBATT5: ("Leak 5 Battery", PERCENTAGE, TYPE_SENSOR, 642 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 643 | STATE_CLASS_MEASUREMENT), 644 | TYPE_LEAKBATT6: ("Leak 6 Battery", PERCENTAGE, TYPE_SENSOR, 645 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 646 | STATE_CLASS_MEASUREMENT), 647 | TYPE_LEAKBATT7: ("Leak 7 Battery", PERCENTAGE, TYPE_SENSOR, 648 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 649 | STATE_CLASS_MEASUREMENT), 650 | TYPE_LEAKBATT8: ("Leak 8 Battery", PERCENTAGE, TYPE_SENSOR, 651 | DEVICE_CLASS_BATTERY, "mdi:battery", 0, 652 | STATE_CLASS_MEASUREMENT), 653 | TYPE_WN34TEMP1C: ("Soil Temperature 1", TEMP_CELSIUS, 654 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 655 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 656 | TYPE_WN34TEMP2C: ("Soil Temperature 2", TEMP_CELSIUS, 657 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 658 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 659 | TYPE_WN34TEMP3C: ("Soil Temperature 3", TEMP_CELSIUS, 660 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 661 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 662 | TYPE_WN34TEMP4C: ("Soil Temperature 4", TEMP_CELSIUS, 663 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 664 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 665 | TYPE_WN34TEMP5C: ("Soil Temperature 5", TEMP_CELSIUS, 666 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 667 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 668 | TYPE_WN34TEMP6C: ("Soil Temperature 6", TEMP_CELSIUS, 669 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 670 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 671 | TYPE_WN34TEMP7C: ("Soil Temperature 7", TEMP_CELSIUS, 672 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 673 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 674 | TYPE_WN34TEMP8C: ("Soil Temperature 8", TEMP_CELSIUS, 675 | TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, 676 | "mdi:thermometer", 0, STATE_CLASS_MEASUREMENT), 677 | TYPE_WN34BATT1: ("Soil Temperature 1 Battery", ELECTRIC_POTENTIAL_VOLT, 678 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, 679 | "mdi:battery", 0, STATE_CLASS_MEASUREMENT), 680 | TYPE_WN34BATT2: ("Soil Temperature 2 Battery", ELECTRIC_POTENTIAL_VOLT, 681 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, 682 | "mdi:battery", 0, STATE_CLASS_MEASUREMENT), 683 | TYPE_WN34BATT3: ("Soil Temperature 3 Battery", ELECTRIC_POTENTIAL_VOLT, 684 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, 685 | "mdi:battery", 0, STATE_CLASS_MEASUREMENT), 686 | TYPE_WN34BATT4: ("Soil Temperature 4 Battery", ELECTRIC_POTENTIAL_VOLT, 687 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, 688 | "mdi:battery", 0, STATE_CLASS_MEASUREMENT), 689 | TYPE_WN34BATT5: ("Soil Temperature 5 Battery", ELECTRIC_POTENTIAL_VOLT, 690 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, 691 | "mdi:battery", 0, STATE_CLASS_MEASUREMENT), 692 | TYPE_WN34BATT6: ("Soil Temperature 6 Battery", ELECTRIC_POTENTIAL_VOLT, 693 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, 694 | "mdi:battery", 0, STATE_CLASS_MEASUREMENT), 695 | TYPE_WN34BATT7: ("Soil Temperature 7 Battery", ELECTRIC_POTENTIAL_VOLT, 696 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, 697 | "mdi:battery", 0, STATE_CLASS_MEASUREMENT), 698 | TYPE_WN34BATT8: ("Soil Temperature 8 Battery", ELECTRIC_POTENTIAL_VOLT, 699 | TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, 700 | "mdi:battery", 0, STATE_CLASS_MEASUREMENT), 701 | } 702 | 703 | IGNORED_SENSORS = [ 704 | 'tempinf', 705 | 'tempf', 706 | 'temp1f', 707 | 'temp2f', 708 | 'temp3f', 709 | 'temp4f', 710 | 'temp5f', 711 | 'temp6f', 712 | 'temp7f', 713 | 'temp8f', 714 | 'tf_co2', 715 | 'tf_ch1', 716 | 'tf_ch2', 717 | 'tf_ch3', 718 | 'tf_ch4', 719 | 'tf_ch5', 720 | 'tf_ch6', 721 | 'tf_ch7', 722 | 'tf_ch8', 723 | 'dateutc', 724 | 'windchillf', 725 | 'dewpointf', 726 | 'dewpointinf', 727 | 'dewpoint1f', 728 | 'dewpoint2f', 729 | 'dewpoint3f', 730 | 'dewpoint4f', 731 | 'dewpoint5f', 732 | 'dewpoint6f', 733 | 'dewpoint7f', 734 | 'dewpoint8f', 735 | 'mac', 736 | 'fields', 737 | DATA_PASSKEY, 738 | DATA_STATIONTYPE, 739 | DATA_FREQ, 740 | DATA_MODEL, 741 | ] 742 | --------------------------------------------------------------------------------