├── LICENSE ├── README.md ├── custom_components └── ha_ecowitt_iot │ ├── __init__.py │ ├── binary_sensor.py │ ├── config_flow.py │ ├── const.py │ ├── coordinator.py │ ├── manifest.json │ ├── sensor.py │ ├── strings.json │ ├── switch.py │ └── translations │ ├── de.json │ └── en.json ├── hacs.json └── img ├── 20240223161726.png ├── 20240223161811.png ├── TF1.jpg ├── TF2.jpg ├── TF3-3.jpg └── TF4.jpg /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ecowitt Official Integration 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Home Assistant - Ecowitt Official Integration 2 | 3 | 4 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration) 5 | 6 | 7 | This integration uses the locally available http APIs to obtain data from the supported devices inside the local network. 8 | 9 | ## :computer: Installation 10 | 11 | ### HACS (Preferred) 12 | This integration can be added to Home Assistant as a [custom HACS repository](https://hacs.xyz/docs/faq/custom_repositories): 13 | 1. From the HACS page, click the 3 dots at the top right corner. 14 | 1. Select `Custom repositories`. 15 | 1. Add the URL `https://github.com/Ecowitt/ha-ecowitt-iot` 16 | 1. Select the category `Integration`. 17 | 1. Click the ADD button. 18 | 1. Restart Home Assistant 19 | 1. Click the button below, or in the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Ecowitt Official Integration" 20 | 21 | [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=Ecowitt&repository=ha-ecowitt-iot&category=integration) 22 | 23 | ### Manual 24 | 1. Download the latest release from [here](https://github.com/Ecowitt/ha-ecowitt-iot/releases). 25 | 1. Create a folder called `custom_components` in the same directory as the Home Assistant `configuration.yaml`. 26 | 1. Extract the contents of the zip into folder called `ha_ecowitt_iot` inside `custom_components`. 27 | 1. Restart Home Assistant 28 | 1. Click the button below, or in the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Ecowitt Official Integration" 29 | 30 | [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=Ecowitt&repository=ha-ecowitt-iot&category=integration) 31 | 32 | ## :bulb: Usage 33 | Ecowitt Official Integration: 34 | This integration uses the locally available HTTP APIs to obtain data from the supported devices inside the local network. 35 | Ecowitt Official Integration Compatibility Instructions: 36 | | Ecowitt Official Integration | IoT device |Gateway Model| 37 | |:-----------:|:-----------:|:-----------| 38 | | × | × |GW1000,WS6006,WN1900,WN1910,WS2320,WS2910,HP2550,HP3500,HP2560| 39 | | ✓ | × |GW1100| 40 | | ✓ | ✓ |GW1200,GW2000,GW3000,WS6210,WN1700,WN1820,WN1821,WN1920,WN1980,WS3800,WS3820,WS3900,WS3910| 41 | 42 | HA Default Integration: www.home-assistant.io/integrations/ecowitt/ 43 | This integration uses the HTTP upload to a 3rd-party to obtain data from the supported devices. 44 | HA Default Integration Compatibility Instructions: 45 | | HA Default Integration | IoT device |Gateway Model| 46 | |:-----------:|:-----------:|:-----------| 47 | | × | × |WS6006| 48 | | ✓ | × |GW1000,WN1900,WN1910,WS2320,WS2910,HP2550,HP3500,HP2560,GW1100,GW1200,GW2000,GW3000,WS6210,WN1700,WN1820,WN1821,WN1920,WN1980,WS3800,WS3820,WS3900,WS3910| 49 | 50 | 51 | To set up Ecowitt Official Integration, follow these steps: 52 | 1. Configure your gateway device on your LAN using the WSView Plus app or Ecowitt app on your phone. 53 | 2. Obtain the device's IP address through the web UI or WSView Plus app. 54 | 3. Enter the device's IP address in the integration. Upon successful connection, the integration will retrieve data from the gateway device. 55 | 56 | 57 | 58 | ![Step 1](./img/TF1.jpg) 59 | ![Step 2](./img/TF2.jpg) 60 | ![Step 3](./img/TF3-3.jpg) 61 | ![Step 4](./img/TF4.jpg) 62 | -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/__init__.py: -------------------------------------------------------------------------------- 1 | """The Ecowitt Official Integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | 7 | from homeassistant.config_entries import ConfigEntry 8 | from homeassistant.const import Platform 9 | from homeassistant.core import HomeAssistant 10 | from homeassistant.const import CONF_HOST 11 | from .const import DOMAIN, CONF_VERSION 12 | from .coordinator import EcowittDataUpdateCoordinator 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.SWITCH] 18 | 19 | 20 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 21 | """Set up Ecowitt Official Integration from a config entry.""" 22 | 23 | coordinator = EcowittDataUpdateCoordinator( 24 | hass, 25 | ) 26 | 27 | await coordinator.async_config_entry_first_refresh() 28 | 29 | hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator 30 | 31 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 32 | 33 | # 注册重新加载函数 34 | entry.async_on_unload(entry.add_update_listener(async_reload_entry)) 35 | 36 | return True 37 | 38 | 39 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 40 | """Unload a config entry.""" 41 | 42 | unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 43 | if unload_ok: 44 | hass.data[DOMAIN].pop(entry.entry_id) 45 | 46 | return unload_ok 47 | 48 | 49 | async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: 50 | """Reload config entry when configuration changes.""" 51 | await hass.config_entries.async_reload(entry.entry_id) 52 | -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/binary_sensor.py: -------------------------------------------------------------------------------- 1 | from homeassistant.components.binary_sensor import ( 2 | BinarySensorEntity, 3 | BinarySensorEntityDescription, 4 | BinarySensorDeviceClass, 5 | ) 6 | import dataclasses 7 | from typing import Final 8 | from wittiot import MultiSensorInfo, WittiotDataTypes 9 | from homeassistant.config_entries import ConfigEntry 10 | from homeassistant.const import ( 11 | CONF_HOST, 12 | EntityCategory, 13 | ) 14 | from homeassistant.core import HomeAssistant 15 | from homeassistant.helpers.device_registry import DeviceInfo 16 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 17 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 18 | from .const import DOMAIN 19 | from .coordinator import EcowittDataUpdateCoordinator 20 | 21 | BINARYSENSOR_DESCRIPTIONS = ( 22 | BinarySensorEntityDescription( 23 | key="srain_piezo", 24 | translation_key="srain_piezo", 25 | icon="mdi:weather-rainy", 26 | device_class=BinarySensorDeviceClass.MOISTURE, # 设备类别为湿度/漏水 27 | ), 28 | ) 29 | 30 | 31 | LEAK_DETECTION_SENSOR: Final = { 32 | WittiotDataTypes.LEAK: BinarySensorEntityDescription( 33 | key="LEAK", 34 | icon="mdi:water-alert", 35 | device_class=BinarySensorDeviceClass.MOISTURE, # 设备类别为湿度/漏水 36 | ), 37 | WittiotDataTypes.BATTERY_BINARY: BinarySensorEntityDescription( 38 | key="BATTERY_BINARY", 39 | icon="mdi:battery", 40 | device_class=BinarySensorDeviceClass.BATTERY, 41 | entity_category=EntityCategory.DIAGNOSTIC, 42 | ), 43 | } 44 | 45 | IOT_BINARYSENSOR_DESCRIPTIONS = ( 46 | BinarySensorEntityDescription( 47 | key="rfnet_state", 48 | translation_key="rfnet_state", 49 | device_class=BinarySensorDeviceClass.CONNECTIVITY, 50 | entity_category=EntityCategory.DIAGNOSTIC, 51 | ), 52 | BinarySensorEntityDescription( 53 | key="iot_running", 54 | translation_key="iot_running", 55 | device_class=BinarySensorDeviceClass.RUNNING, 56 | entity_category=EntityCategory.DIAGNOSTIC, 57 | ), 58 | ) 59 | 60 | 61 | # 在设备设置函数中创建实体 62 | async def async_setup_entry( 63 | hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback 64 | ) -> None: 65 | """设置二进制传感器平台.""" 66 | coordinator = hass.data[DOMAIN][entry.entry_id] 67 | 68 | # 添加普通传感器 69 | async_add_entities( 70 | MainDevEcowittBinarySensor(coordinator, entry.unique_id, desc) 71 | for desc in BINARYSENSOR_DESCRIPTIONS 72 | if desc.key in coordinator.data 73 | ) 74 | 75 | # Subdevice Data 76 | binary_sensors: list[SubDevEcowittBinarySensor] = [] 77 | for key in coordinator.data: 78 | if key in MultiSensorInfo.SENSOR_INFO and MultiSensorInfo.SENSOR_INFO[key][ 79 | "data_type" 80 | ] in (WittiotDataTypes.LEAK, WittiotDataTypes.BATTERY_BINARY): 81 | mapping = LEAK_DETECTION_SENSOR[ 82 | MultiSensorInfo.SENSOR_INFO[key]["data_type"] 83 | ] 84 | description = dataclasses.replace( 85 | mapping, 86 | key=key, 87 | name=MultiSensorInfo.SENSOR_INFO[key]["name"], 88 | ) 89 | binary_sensors.append( 90 | SubDevEcowittBinarySensor( 91 | coordinator, 92 | entry.unique_id, 93 | MultiSensorInfo.SENSOR_INFO[key]["dev_type"], 94 | description, 95 | ) 96 | ) 97 | async_add_entities(binary_sensors) 98 | 99 | if "iot_list" in coordinator.data: 100 | iot_sensors: list[IotDeviceBinarySensor] = [] 101 | desc_map = {desc.key: desc for desc in IOT_BINARYSENSOR_DESCRIPTIONS} 102 | iot_data = coordinator.data["iot_list"] 103 | commands = iot_data["command"] 104 | for i, item in enumerate(commands): 105 | nickname = item.get("nickname") 106 | if nickname is None: 107 | continue 108 | for key in list(item): 109 | if key in desc_map: 110 | desc = desc_map[key] 111 | device_desc = dataclasses.replace( 112 | desc, 113 | key=f"{nickname}_{desc.key}", 114 | # name=f"{device_info.get('name', f'设备 {device_id}')} {desc.name}", 115 | ) 116 | # 添加到实体列表 117 | iot_sensors.append( 118 | IotDeviceBinarySensor( 119 | coordinator=coordinator, 120 | device_id=nickname, 121 | description=device_desc, 122 | unique_id=entry.unique_id, 123 | ) 124 | ) 125 | async_add_entities(iot_sensors) 126 | 127 | 128 | class MainDevEcowittBinarySensor( 129 | CoordinatorEntity[EcowittDataUpdateCoordinator], # 继承 CoordinatorEntity 130 | BinarySensorEntity, # 继承 BinarySensorEntity 131 | ): 132 | """Ecowitt 漏水检测二进制传感器.""" 133 | 134 | _attr_has_entity_name = True 135 | 136 | def __init__( 137 | self, 138 | coordinator: EcowittDataUpdateCoordinator, 139 | device_name: str, 140 | description: BinarySensorEntityDescription, 141 | ) -> None: 142 | """初始化漏水检测传感器.""" 143 | super().__init__(coordinator) 144 | 145 | # 设置设备信息 146 | self._attr_device_info = DeviceInfo( 147 | identifiers={(DOMAIN, f"{device_name}")}, 148 | manufacturer="Ecowitt", 149 | name=f"{device_name}", 150 | model=coordinator.data["ver"], 151 | configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", 152 | ) 153 | 154 | # 设置唯一ID和实体描述 155 | self._attr_unique_id = f"{device_name}_{description.key}" 156 | self.entity_description = description 157 | self._sensor_key = description.key # 存储用于数据访问的键 158 | 159 | @property 160 | def is_on(self) -> bool | None: 161 | """返回二进制传感器状态 (True 表示检测到漏水).""" 162 | # 从协调器获取当前数据 163 | value = self.coordinator.data.get(self.entity_description.key) 164 | if value is not None: 165 | return value != "No rain" 166 | return None # 如果数据不可用返回None 167 | 168 | @property 169 | def available(self) -> bool: 170 | """实体是否可用""" 171 | return super().available and self._sensor_key in self.coordinator.data 172 | 173 | 174 | class SubDevEcowittBinarySensor( 175 | CoordinatorEntity[EcowittDataUpdateCoordinator], # 继承 CoordinatorEntity 176 | BinarySensorEntity, # 继承 BinarySensorEntity 177 | ): 178 | """Ecowitt 漏水检测二进制传感器.""" 179 | 180 | _attr_has_entity_name = True 181 | 182 | def __init__( 183 | self, 184 | coordinator: EcowittDataUpdateCoordinator, 185 | device_name: str, 186 | sensor_type: str, 187 | description: BinarySensorEntityDescription, 188 | ) -> None: 189 | """初始化漏水检测传感器.""" 190 | super().__init__(coordinator) 191 | self._attr_device_info = DeviceInfo( 192 | identifiers={(DOMAIN, f"{device_name}")}, 193 | manufacturer="Ecowitt", 194 | name=f"{device_name}", 195 | model=coordinator.data["ver"], 196 | configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", 197 | ) 198 | 199 | # 设置唯一ID和实体描述 200 | self._attr_unique_id = f"{device_name}_{description.key}" 201 | self.entity_description = description 202 | self._sensor_key = description.key # 存储用于数据访问的键 203 | 204 | @property 205 | def is_on(self) -> bool | None: 206 | """返回二进制传感器状态 (True 表示检测到漏水).""" 207 | # 从协调器获取当前数据 208 | leak_value = self.coordinator.data.get(self.entity_description.key) 209 | if leak_value is not None: 210 | return leak_value != "Normal" # 1=漏水(True),0=正常(False) 211 | return None # 如果数据不可用返回None 212 | 213 | @property 214 | def available(self) -> bool: 215 | """实体是否可用""" 216 | return super().available and self._sensor_key in self.coordinator.data 217 | 218 | 219 | class IotDeviceBinarySensor(CoordinatorEntity, BinarySensorEntity): 220 | """表示 IoT 设备的传感器实体""" 221 | 222 | _attr_has_entity_name = True 223 | 224 | def __init__( 225 | self, 226 | coordinator: EcowittDataUpdateCoordinator, 227 | device_id: str, 228 | description: BinarySensorEntityDescription, 229 | unique_id: str, 230 | ) -> None: 231 | """初始化 IoT 设备传感器""" 232 | super().__init__(coordinator) 233 | self.device_id = device_id 234 | self.entity_description = description 235 | self._attr_unique_id = f"{device_id}_{description.key}" 236 | 237 | # 设置设备信息 238 | self._attr_device_info = DeviceInfo( 239 | identifiers={(DOMAIN, f"{device_id}")}, 240 | name=f"{device_id}", 241 | manufacturer="Ecowitt", 242 | model=coordinator.data["ver"], 243 | configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", 244 | via_device=(DOMAIN, unique_id), 245 | ) 246 | 247 | @property 248 | def is_on(self) -> bool | None: 249 | """返回二进制传感器状态 (True 表示检测到漏水).""" 250 | """获取传感器值""" 251 | # # 从协调器获取设备数据 252 | if "iot_list" in self.coordinator.data: 253 | iot_data = self.coordinator.data["iot_list"] 254 | commands = iot_data["command"] 255 | for i, item in enumerate(commands): 256 | nickname = item.get("nickname") 257 | if nickname is None: 258 | continue 259 | if nickname == self.device_id: 260 | key = self.entity_description.key.split("_", 1)[1] 261 | return item.get(key, None) 262 | return None # 如果数据不可用返回None 263 | -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Ecowitt Official Integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | from typing import Any 7 | import voluptuous as vol 8 | from wittiot import API 9 | from wittiot.errors import WittiotError 10 | 11 | from homeassistant import config_entries 12 | from homeassistant.const import CONF_HOST 13 | from homeassistant.data_entry_flow import FlowResult 14 | from homeassistant.helpers import aiohttp_client 15 | 16 | from .const import DOMAIN 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | 21 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 22 | """Handle a config flow for Ecowitt Official Integration.""" 23 | 24 | async def async_step_user( 25 | self, user_input: dict[str, Any] | None = None 26 | ) -> FlowResult: 27 | """Handle the local step.""" 28 | errors = {} 29 | 30 | if user_input is not None: 31 | api = API( 32 | user_input[CONF_HOST], 33 | session=aiohttp_client.async_get_clientsession(self.hass), 34 | ) 35 | 36 | try: 37 | devices = await api.request_loc_info() 38 | except WittiotError: 39 | errors["base"] = "cannot_connect" 40 | _LOGGER.debug("New data received: %s", devices) 41 | 42 | if not devices: 43 | errors["base"] = "no_devices" 44 | 45 | if not errors: 46 | unique_id = devices["dev_name"] 47 | await self.async_set_unique_id(unique_id) 48 | self._abort_if_unique_id_configured() 49 | 50 | return self.async_create_entry(title=unique_id, data=user_input) 51 | 52 | return self.async_show_form( 53 | step_id="user", 54 | data_schema=vol.Schema({vol.Required(CONF_HOST): str}), 55 | errors=errors, 56 | ) 57 | 58 | @staticmethod 59 | def async_get_options_flow(config_entry): 60 | """Get the options flow for this handler.""" 61 | return OptionsFlowHandler(config_entry) 62 | 63 | 64 | class OptionsFlowHandler(config_entries.OptionsFlow): 65 | """Handle options flow for Ecowitt integration.""" 66 | 67 | def __init__(self, config_entry: config_entries.ConfigEntry) -> None: 68 | """Initialize options flow.""" 69 | # self.config_entry = config_entry 70 | 71 | async def async_step_init( 72 | self, user_input: dict[str, Any] | None = None 73 | ) -> FlowResult: 74 | """Manage the options.""" 75 | errors = {} 76 | 77 | if user_input is not None: 78 | # 验证新的设置 79 | api = API( 80 | user_input[CONF_HOST], 81 | session=aiohttp_client.async_get_clientsession(self.hass), 82 | ) 83 | 84 | try: 85 | devices = await api.request_loc_info() 86 | except WittiotError: 87 | errors["base"] = "cannot_connect" 88 | else: 89 | if not devices: 90 | errors["base"] = "no_devices" 91 | else: 92 | # 更新配置条目 93 | new_data = {**self.config_entry.data, **user_input} 94 | self.hass.config_entries.async_update_entry( 95 | self.config_entry, data=new_data 96 | ) 97 | 98 | # 如果需要,也可以更新唯一ID和标题 99 | unique_id = devices["dev_name"] 100 | if unique_id != self.config_entry.unique_id: 101 | self.hass.config_entries.async_update_entry( 102 | self.config_entry, unique_id=unique_id, title=unique_id 103 | ) 104 | 105 | return self.async_create_entry(title="", data={}) 106 | 107 | # 显示表单,预填当前值 108 | return self.async_show_form( 109 | step_id="init", 110 | data_schema=vol.Schema( 111 | { 112 | vol.Required( 113 | CONF_HOST, default=self.config_entry.data.get(CONF_HOST) 114 | ): str 115 | } 116 | ), 117 | errors=errors, 118 | ) 119 | -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Ecowitt Official Integration.""" 2 | 3 | DOMAIN = "ha_ecowitt_iot" 4 | CONF_VERSION = 2 5 | -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/coordinator.py: -------------------------------------------------------------------------------- 1 | """The Ecowitt integration coordinator.""" 2 | from __future__ import annotations 3 | 4 | from datetime import timedelta 5 | import logging 6 | from typing import Any 7 | 8 | from aiohttp.client_exceptions import ClientConnectorError 9 | from wittiot import API 10 | from wittiot.errors import WittiotError 11 | 12 | from homeassistant.config_entries import ConfigEntry 13 | from homeassistant.const import CONF_HOST 14 | from homeassistant.core import HomeAssistant 15 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 16 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 17 | 18 | from .const import DOMAIN 19 | 20 | SCAN_INTERVAL = timedelta(seconds=60) 21 | 22 | _LOGGER = logging.getLogger(__name__) 23 | 24 | 25 | class EcowittDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): 26 | """Define an object to hold Ecowitt data.""" 27 | 28 | config_entry: ConfigEntry 29 | 30 | def __init__( 31 | self, 32 | hass: HomeAssistant, 33 | ) -> None: 34 | """Initialize.""" 35 | super().__init__( 36 | hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=10) 37 | ) 38 | self.api = API( 39 | self.config_entry.data[CONF_HOST], session=async_get_clientsession(hass) 40 | ) 41 | 42 | async def _async_update_data(self) -> dict[str, str | float | int]: 43 | """Update data.""" 44 | res = {} 45 | try: 46 | res = await self.api.request_loc_allinfo() 47 | except (WittiotError, ClientConnectorError) as error: 48 | raise UpdateFailed(error) from error 49 | # _LOGGER.info("Get device data: %s", res) 50 | return res 51 | -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "ha_ecowitt_iot", 3 | "name": "Ecowitt Official Integration", 4 | "codeowners": ["@Ecowitt"], 5 | "config_flow": true, 6 | "dependencies": [], 7 | "documentation": "https://github.com/Ecowitt/ha-ecowitt-iot", 8 | "homekit": {}, 9 | "iot_class": "local_polling", 10 | "issue_tracker": "https://github.com/Ecowitt/ha-ecowitt-iot/issues", 11 | "requirements": ["wittiot==1.0.30"], 12 | "ssdp": [], 13 | "version": "1.0.0", 14 | "zeroconf": [] 15 | } -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/sensor.py: -------------------------------------------------------------------------------- 1 | """Platform for sensor integration.""" 2 | 3 | import dataclasses 4 | from typing import Final 5 | import logging 6 | from wittiot import MultiSensorInfo, WittiotDataTypes, SubSensorname 7 | from homeassistant.components.sensor import ( 8 | SensorDeviceClass, 9 | SensorEntity, 10 | SensorEntityDescription, 11 | SensorStateClass, 12 | ) 13 | from homeassistant.config_entries import ConfigEntry 14 | from homeassistant.const import ( 15 | CONCENTRATION_PARTS_PER_MILLION, 16 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 17 | CONF_HOST, 18 | DEGREE, 19 | PERCENTAGE, 20 | SIGNAL_STRENGTH_DECIBELS_MILLIWATT, 21 | UnitOfTime, 22 | UnitOfPower, 23 | UnitOfEnergy, 24 | UnitOfElectricPotential, 25 | UnitOfVolume, 26 | UnitOfVolumeFlowRate, 27 | UnitOfElectricPotential, 28 | UnitOfIrradiance, 29 | UnitOfLength, 30 | UnitOfPrecipitationDepth, 31 | UnitOfPressure, 32 | UnitOfSpeed, 33 | UnitOfTemperature, 34 | UnitOfVolumetricFlux, 35 | ) 36 | from homeassistant.core import HomeAssistant 37 | from homeassistant.helpers.device_registry import DeviceInfo 38 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 39 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 40 | from homeassistant.helpers.entity import EntityCategory 41 | from .const import DOMAIN 42 | from .coordinator import EcowittDataUpdateCoordinator 43 | from homeassistant.helpers import device_registry as dr 44 | 45 | _LOGGER = logging.getLogger(__name__) 46 | 47 | SENSOR_DESCRIPTIONS = ( 48 | SensorEntityDescription( 49 | key="tempinf", 50 | translation_key="tempinf", 51 | native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, 52 | device_class=SensorDeviceClass.TEMPERATURE, 53 | state_class=SensorStateClass.MEASUREMENT, 54 | ), 55 | SensorEntityDescription( 56 | key="tempf", 57 | translation_key="tempf", 58 | native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, 59 | device_class=SensorDeviceClass.TEMPERATURE, 60 | state_class=SensorStateClass.MEASUREMENT, 61 | ), 62 | SensorEntityDescription( 63 | key="feellike", 64 | translation_key="feellike", 65 | native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, 66 | device_class=SensorDeviceClass.TEMPERATURE, 67 | state_class=SensorStateClass.MEASUREMENT, 68 | ), 69 | SensorEntityDescription( 70 | key="apparent", 71 | translation_key="apparent", 72 | native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, 73 | device_class=SensorDeviceClass.TEMPERATURE, 74 | state_class=SensorStateClass.MEASUREMENT, 75 | ), 76 | SensorEntityDescription( 77 | key="dewpoint", 78 | translation_key="dewpoint", 79 | native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, 80 | device_class=SensorDeviceClass.TEMPERATURE, 81 | state_class=SensorStateClass.MEASUREMENT, 82 | ), 83 | SensorEntityDescription( 84 | key="tf_co2", 85 | translation_key="tf_co2", 86 | native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, 87 | device_class=SensorDeviceClass.TEMPERATURE, 88 | state_class=SensorStateClass.MEASUREMENT, 89 | ), 90 | SensorEntityDescription( 91 | key="humidityin", 92 | translation_key="humidityin", 93 | native_unit_of_measurement=PERCENTAGE, 94 | device_class=SensorDeviceClass.HUMIDITY, 95 | state_class=SensorStateClass.MEASUREMENT, 96 | ), 97 | SensorEntityDescription( 98 | key="humidity", 99 | translation_key="humidity", 100 | native_unit_of_measurement=PERCENTAGE, 101 | device_class=SensorDeviceClass.HUMIDITY, 102 | state_class=SensorStateClass.MEASUREMENT, 103 | ), 104 | SensorEntityDescription( 105 | key="humi_co2", 106 | translation_key="humi_co2", 107 | native_unit_of_measurement=PERCENTAGE, 108 | device_class=SensorDeviceClass.HUMIDITY, 109 | state_class=SensorStateClass.MEASUREMENT, 110 | ), 111 | SensorEntityDescription( 112 | key="baromrelin", 113 | translation_key="baromrelin", 114 | native_unit_of_measurement=UnitOfPressure.INHG, 115 | device_class=SensorDeviceClass.PRESSURE, 116 | ), 117 | SensorEntityDescription( 118 | key="baromabsin", 119 | translation_key="baromabsin", 120 | native_unit_of_measurement=UnitOfPressure.INHG, 121 | device_class=SensorDeviceClass.PRESSURE, 122 | ), 123 | SensorEntityDescription( 124 | key="vpd", 125 | translation_key="vpd", 126 | native_unit_of_measurement=UnitOfPressure.INHG, 127 | device_class=SensorDeviceClass.PRESSURE, 128 | ), 129 | SensorEntityDescription( 130 | key="winddir", 131 | translation_key="winddir", 132 | icon="mdi:weather-windy", 133 | native_unit_of_measurement=DEGREE, 134 | ), 135 | SensorEntityDescription( 136 | key="winddir10", 137 | translation_key="winddir10", 138 | icon="mdi:weather-windy", 139 | native_unit_of_measurement=DEGREE, 140 | ), 141 | SensorEntityDescription( 142 | key="windspeedmph", 143 | translation_key="windspeedmph", 144 | device_class=SensorDeviceClass.WIND_SPEED, 145 | native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, 146 | state_class=SensorStateClass.MEASUREMENT, 147 | ), 148 | SensorEntityDescription( 149 | key="windgustmph", 150 | translation_key="windgustmph", 151 | device_class=SensorDeviceClass.WIND_SPEED, 152 | native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, 153 | state_class=SensorStateClass.MEASUREMENT, 154 | ), 155 | SensorEntityDescription( 156 | key="daywindmax", 157 | translation_key="daywindmax", 158 | device_class=SensorDeviceClass.WIND_SPEED, 159 | native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, 160 | state_class=SensorStateClass.MEASUREMENT, 161 | ), 162 | SensorEntityDescription( 163 | key="uv", 164 | translation_key="uv", 165 | state_class=SensorStateClass.MEASUREMENT, 166 | icon="mdi:brightness-5", 167 | ), 168 | SensorEntityDescription( 169 | key="solarradiation", 170 | translation_key="solarradiation", 171 | native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, 172 | device_class=SensorDeviceClass.IRRADIANCE, 173 | state_class=SensorStateClass.MEASUREMENT, 174 | ), 175 | SensorEntityDescription( 176 | key="rainratein", 177 | translation_key="rainratein", 178 | native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR, 179 | device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, 180 | state_class=SensorStateClass.MEASUREMENT, 181 | suggested_display_precision=2, 182 | ), 183 | SensorEntityDescription( 184 | key="eventrainin", 185 | translation_key="eventrainin", 186 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 187 | device_class=SensorDeviceClass.PRECIPITATION, 188 | state_class=SensorStateClass.MEASUREMENT, 189 | suggested_display_precision=2, 190 | ), 191 | SensorEntityDescription( 192 | key="dailyrainin", 193 | translation_key="dailyrainin", 194 | device_class=SensorDeviceClass.PRECIPITATION, 195 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 196 | state_class=SensorStateClass.TOTAL, 197 | suggested_display_precision=2, 198 | ), 199 | SensorEntityDescription( 200 | key="weeklyrainin", 201 | translation_key="weeklyrainin", 202 | device_class=SensorDeviceClass.PRECIPITATION, 203 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 204 | state_class=SensorStateClass.TOTAL, 205 | suggested_display_precision=2, 206 | ), 207 | SensorEntityDescription( 208 | key="monthlyrainin", 209 | translation_key="monthlyrainin", 210 | device_class=SensorDeviceClass.PRECIPITATION, 211 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 212 | state_class=SensorStateClass.TOTAL, 213 | suggested_display_precision=2, 214 | ), 215 | SensorEntityDescription( 216 | key="yearlyrainin", 217 | translation_key="yearlyrainin", 218 | device_class=SensorDeviceClass.PRECIPITATION, 219 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 220 | state_class=SensorStateClass.TOTAL, 221 | suggested_display_precision=2, 222 | ), 223 | SensorEntityDescription( 224 | key="totalrainin", 225 | translation_key="totalrainin", 226 | device_class=SensorDeviceClass.PRECIPITATION, 227 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 228 | state_class=SensorStateClass.TOTAL, 229 | suggested_display_precision=2, 230 | ), 231 | SensorEntityDescription( 232 | key="24hrainin", 233 | translation_key="24hrainin", 234 | device_class=SensorDeviceClass.PRECIPITATION, 235 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 236 | state_class=SensorStateClass.TOTAL, 237 | suggested_display_precision=2, 238 | ), 239 | SensorEntityDescription( 240 | key="rrain_piezo", 241 | translation_key="rrain_piezo", 242 | native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR, 243 | device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, 244 | state_class=SensorStateClass.MEASUREMENT, 245 | suggested_display_precision=2, 246 | ), 247 | SensorEntityDescription( 248 | key="erain_piezo", 249 | translation_key="erain_piezo", 250 | device_class=SensorDeviceClass.PRECIPITATION, 251 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 252 | state_class=SensorStateClass.MEASUREMENT, 253 | suggested_display_precision=2, 254 | ), 255 | SensorEntityDescription( 256 | key="drain_piezo", 257 | translation_key="drain_piezo", 258 | device_class=SensorDeviceClass.PRECIPITATION, 259 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 260 | state_class=SensorStateClass.TOTAL, 261 | suggested_display_precision=2, 262 | ), 263 | SensorEntityDescription( 264 | key="wrain_piezo", 265 | translation_key="wrain_piezo", 266 | device_class=SensorDeviceClass.PRECIPITATION, 267 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 268 | state_class=SensorStateClass.TOTAL, 269 | suggested_display_precision=2, 270 | ), 271 | SensorEntityDescription( 272 | key="mrain_piezo", 273 | translation_key="mrain_piezo", 274 | device_class=SensorDeviceClass.PRECIPITATION, 275 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 276 | state_class=SensorStateClass.TOTAL, 277 | suggested_display_precision=2, 278 | ), 279 | SensorEntityDescription( 280 | key="yrain_piezo", 281 | translation_key="yrain_piezo", 282 | device_class=SensorDeviceClass.PRECIPITATION, 283 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 284 | state_class=SensorStateClass.TOTAL, 285 | suggested_display_precision=2, 286 | ), 287 | SensorEntityDescription( 288 | key="train_piezo", 289 | translation_key="train_piezo", 290 | device_class=SensorDeviceClass.PRECIPITATION, 291 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 292 | state_class=SensorStateClass.TOTAL, 293 | suggested_display_precision=2, 294 | ), 295 | SensorEntityDescription( 296 | key="24hrain_piezo", 297 | translation_key="24hrain_piezo", 298 | device_class=SensorDeviceClass.PRECIPITATION, 299 | native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, 300 | state_class=SensorStateClass.TOTAL, 301 | suggested_display_precision=2, 302 | ), 303 | SensorEntityDescription( 304 | key="co2in", 305 | translation_key="co2in", 306 | native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, 307 | device_class=SensorDeviceClass.CO2, 308 | state_class=SensorStateClass.MEASUREMENT, 309 | ), 310 | SensorEntityDescription( 311 | key="co2", 312 | native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, 313 | device_class=SensorDeviceClass.CO2, 314 | state_class=SensorStateClass.MEASUREMENT, 315 | ), 316 | SensorEntityDescription( 317 | key="co2in_24h", 318 | translation_key="co2in_24h", 319 | native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, 320 | device_class=SensorDeviceClass.CO2, 321 | state_class=SensorStateClass.MEASUREMENT, 322 | ), 323 | SensorEntityDescription( 324 | key="co2_24h", 325 | translation_key="co2_24h", 326 | native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, 327 | device_class=SensorDeviceClass.CO2, 328 | state_class=SensorStateClass.MEASUREMENT, 329 | ), 330 | SensorEntityDescription( 331 | key="pm25_co2", 332 | translation_key="pm25_co2", 333 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 334 | device_class=SensorDeviceClass.PM25, 335 | state_class=SensorStateClass.MEASUREMENT, 336 | ), 337 | SensorEntityDescription( 338 | key="pm25_aqi_co2", 339 | translation_key="pm25_aqi_co2", 340 | device_class=SensorDeviceClass.AQI, 341 | state_class=SensorStateClass.MEASUREMENT, 342 | ), 343 | SensorEntityDescription( 344 | key="pm25_24h_co2", 345 | translation_key="pm25_24h_co2", 346 | device_class=SensorDeviceClass.AQI, 347 | state_class=SensorStateClass.MEASUREMENT, 348 | ), 349 | SensorEntityDescription( 350 | key="pm10_co2", 351 | translation_key="pm10_co2", 352 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 353 | device_class=SensorDeviceClass.PM10, 354 | state_class=SensorStateClass.MEASUREMENT, 355 | ), 356 | SensorEntityDescription( 357 | key="pm10_aqi_co2", 358 | translation_key="pm10_aqi_co2", 359 | device_class=SensorDeviceClass.AQI, 360 | state_class=SensorStateClass.MEASUREMENT, 361 | ), 362 | SensorEntityDescription( 363 | key="pm10_24h_co2", 364 | translation_key="pm10_24h_co2", 365 | device_class=SensorDeviceClass.AQI, 366 | state_class=SensorStateClass.MEASUREMENT, 367 | ), 368 | SensorEntityDescription( 369 | key="pm1_24h_co2_add", 370 | translation_key="pm1_24h_co2_add", 371 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 372 | device_class=SensorDeviceClass.PM1, 373 | state_class=SensorStateClass.MEASUREMENT, 374 | ), 375 | SensorEntityDescription( 376 | key="pm4_24h_co2_add", 377 | translation_key="pm4_24h_co2_add", 378 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 379 | device_class=SensorDeviceClass.PM25, 380 | state_class=SensorStateClass.MEASUREMENT, 381 | ), 382 | SensorEntityDescription( 383 | key="pm25_24h_co2_add", 384 | translation_key="pm25_24h_co2_add", 385 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 386 | device_class=SensorDeviceClass.PM25, 387 | state_class=SensorStateClass.MEASUREMENT, 388 | ), 389 | SensorEntityDescription( 390 | key="pm10_24h_co2_add", 391 | translation_key="pm10_24h_co2_add", 392 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 393 | device_class=SensorDeviceClass.PM10, 394 | state_class=SensorStateClass.MEASUREMENT, 395 | ), 396 | SensorEntityDescription( 397 | key="lightning", 398 | translation_key="lightning", 399 | icon="mdi:lightning-bolt", 400 | native_unit_of_measurement=UnitOfLength.MILES, 401 | device_class=SensorDeviceClass.DISTANCE, 402 | state_class=SensorStateClass.MEASUREMENT, 403 | ), 404 | SensorEntityDescription( 405 | key="lightning_time", 406 | translation_key="lightning_time", 407 | icon="mdi:lightning-bolt", 408 | ), 409 | SensorEntityDescription( 410 | key="lightning_num", 411 | translation_key="lightning_num", 412 | icon="mdi:lightning-bolt", 413 | state_class=SensorStateClass.TOTAL, 414 | ), 415 | SensorEntityDescription( 416 | key="con_batt", 417 | translation_key="con_batt", 418 | icon="mdi:battery", 419 | entity_category=EntityCategory.DIAGNOSTIC, 420 | ), 421 | SensorEntityDescription( 422 | key="con_batt_volt", 423 | translation_key="con_batt_volt", 424 | icon="mdi:battery", 425 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 426 | device_class=SensorDeviceClass.VOLTAGE, 427 | state_class=SensorStateClass.MEASUREMENT, 428 | suggested_display_precision=2, 429 | ), 430 | SensorEntityDescription( 431 | key="con_ext_volt", 432 | translation_key="con_ext_volt", 433 | icon="mdi:battery", 434 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 435 | device_class=SensorDeviceClass.VOLTAGE, 436 | state_class=SensorStateClass.MEASUREMENT, 437 | suggested_display_precision=2, 438 | ), 439 | SensorEntityDescription( 440 | key="piezora_batt", 441 | translation_key="piezora_batt", 442 | icon="mdi:battery", 443 | entity_category=EntityCategory.DIAGNOSTIC, 444 | ), 445 | # SensorEntityDescription( 446 | # key="srain_piezo", 447 | # translation_key="srain_piezo", 448 | # icon="mdi:weather-rainy", 449 | # ), 450 | SensorEntityDescription( 451 | key="pm1_co2", 452 | translation_key="pm1_co2", 453 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 454 | device_class=SensorDeviceClass.PM1, 455 | state_class=SensorStateClass.MEASUREMENT, 456 | ), 457 | SensorEntityDescription( 458 | key="pm1_aqi_co2", 459 | translation_key="pm1_aqi_co2", 460 | device_class=SensorDeviceClass.AQI, 461 | state_class=SensorStateClass.MEASUREMENT, 462 | ), 463 | SensorEntityDescription( 464 | key="pm1_24h_co2", 465 | translation_key="pm1_24h_co2", 466 | device_class=SensorDeviceClass.AQI, 467 | state_class=SensorStateClass.MEASUREMENT, 468 | ), 469 | SensorEntityDescription( 470 | key="pm4_co2", 471 | translation_key="pm4_co2", 472 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 473 | device_class=SensorDeviceClass.PM25, 474 | state_class=SensorStateClass.MEASUREMENT, 475 | ), 476 | SensorEntityDescription( 477 | key="pm4_aqi_co2", 478 | translation_key="pm4_aqi_co2", 479 | device_class=SensorDeviceClass.AQI, 480 | state_class=SensorStateClass.MEASUREMENT, 481 | ), 482 | SensorEntityDescription( 483 | key="pm4_24h_co2", 484 | translation_key="pm4_24h_co2", 485 | device_class=SensorDeviceClass.AQI, 486 | state_class=SensorStateClass.MEASUREMENT, 487 | ), 488 | ) 489 | 490 | ECOWITT_SENSORS_MAPPING: Final = { 491 | WittiotDataTypes.TEMPERATURE: SensorEntityDescription( 492 | key="TEMPERATURE", 493 | native_unit_of_measurement="°F", 494 | device_class=SensorDeviceClass.TEMPERATURE, 495 | state_class=SensorStateClass.MEASUREMENT, 496 | ), 497 | WittiotDataTypes.HUMIDITY: SensorEntityDescription( 498 | key="HUMIDITY", 499 | native_unit_of_measurement=PERCENTAGE, 500 | device_class=SensorDeviceClass.HUMIDITY, 501 | state_class=SensorStateClass.MEASUREMENT, 502 | ), 503 | WittiotDataTypes.PM25: SensorEntityDescription( 504 | key="PM25", 505 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 506 | device_class=SensorDeviceClass.PM25, 507 | state_class=SensorStateClass.MEASUREMENT, 508 | ), 509 | WittiotDataTypes.AQI: SensorEntityDescription( 510 | key="AQI", 511 | device_class=SensorDeviceClass.AQI, 512 | state_class=SensorStateClass.MEASUREMENT, 513 | ), 514 | WittiotDataTypes.LEAK: SensorEntityDescription( 515 | key="LEAK", 516 | icon="mdi:water-alert", 517 | ), 518 | WittiotDataTypes.BATTERY: SensorEntityDescription( 519 | key="BATTERY", 520 | icon="mdi:battery", 521 | entity_category=EntityCategory.DIAGNOSTIC, 522 | ), 523 | WittiotDataTypes.DISTANCE: SensorEntityDescription( 524 | key="DISTANCE", 525 | native_unit_of_measurement=UnitOfLength.FEET, 526 | device_class=SensorDeviceClass.DISTANCE, 527 | state_class=SensorStateClass.MEASUREMENT, 528 | suggested_display_precision=2, 529 | icon="mdi:arrow-expand-vertical", 530 | ), 531 | WittiotDataTypes.HEAT: SensorEntityDescription( 532 | key="HEAT", 533 | state_class=SensorStateClass.MEASUREMENT, 534 | ), 535 | WittiotDataTypes.BATTERY_BINARY: SensorEntityDescription( 536 | key="BATTERY_BINARY", 537 | icon="mdi:battery", 538 | entity_category=EntityCategory.DIAGNOSTIC, 539 | ), 540 | WittiotDataTypes.SIGNAL: SensorEntityDescription( 541 | key="SIGNAL", 542 | state_class=SensorStateClass.MEASUREMENT, 543 | entity_category=EntityCategory.DIAGNOSTIC, 544 | icon="mdi:signal", 545 | ), 546 | WittiotDataTypes.RSSI: SensorEntityDescription( 547 | key="RSSI", 548 | state_class=SensorStateClass.MEASUREMENT, 549 | native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, 550 | entity_category=EntityCategory.DIAGNOSTIC, 551 | icon="mdi:wifi", 552 | ), 553 | } 554 | # 定义 IoT 设备的传感器描述 555 | IOT_SENSOR_DESCRIPTIONS = ( 556 | SensorEntityDescription( 557 | key="iotbatt", 558 | translation_key="iotbatt", 559 | icon="mdi:battery", 560 | entity_category=EntityCategory.DIAGNOSTIC, 561 | ), 562 | SensorEntityDescription( 563 | key="signal", 564 | translation_key="signal", 565 | entity_category=EntityCategory.DIAGNOSTIC, 566 | icon="mdi:signal", 567 | ), 568 | # SensorEntityDescription( 569 | # key="rfnet_state", 570 | # translation_key="rfnet_state", 571 | # entity_category=EntityCategory.DIAGNOSTIC, 572 | # ), 573 | # SensorEntityDescription( 574 | # key="iot_running", 575 | # translation_key="iot_running", 576 | # entity_category=EntityCategory.DIAGNOSTIC, 577 | # ), 578 | SensorEntityDescription( 579 | key="run_time", 580 | translation_key="run_time", 581 | entity_category=EntityCategory.DIAGNOSTIC, 582 | native_unit_of_measurement=UnitOfTime.SECONDS, 583 | device_class=SensorDeviceClass.DURATION, 584 | ), 585 | SensorEntityDescription( 586 | key="ver", 587 | translation_key="ver", 588 | entity_category=EntityCategory.DIAGNOSTIC, 589 | ), 590 | SensorEntityDescription( 591 | key="wfc02_position", 592 | translation_key="wfc02_position", 593 | icon="mdi:valve", 594 | native_unit_of_measurement=PERCENTAGE, 595 | ), 596 | SensorEntityDescription( 597 | key="wfc02_flow_velocity", 598 | translation_key="wfc02_flow_velocity", 599 | native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE, 600 | device_class=SensorDeviceClass.VOLUME_FLOW_RATE, 601 | ), 602 | SensorEntityDescription( 603 | key="velocity_total", 604 | translation_key="velocity_total", 605 | native_unit_of_measurement=UnitOfVolume.LITERS, 606 | device_class=SensorDeviceClass.WATER, 607 | ), 608 | SensorEntityDescription( 609 | key="flow_velocity", 610 | translation_key="flow_velocity", 611 | native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE, 612 | device_class=SensorDeviceClass.VOLUME_FLOW_RATE, 613 | ), 614 | SensorEntityDescription( 615 | key="data_water_t", 616 | translation_key="data_water_t", 617 | native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, 618 | device_class=SensorDeviceClass.TEMPERATURE, 619 | state_class=SensorStateClass.MEASUREMENT, 620 | ), 621 | SensorEntityDescription( 622 | key="data_ac_v", 623 | translation_key="data_ac_v", 624 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 625 | device_class=SensorDeviceClass.VOLTAGE, 626 | ), 627 | SensorEntityDescription( 628 | key="elect_total", 629 | translation_key="elect_total", 630 | native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, 631 | device_class=SensorDeviceClass.ENERGY, 632 | state_class=SensorStateClass.TOTAL, 633 | ), 634 | SensorEntityDescription( 635 | key="realtime_power", 636 | translation_key="realtime_power", 637 | native_unit_of_measurement=UnitOfPower.WATT, 638 | device_class=SensorDeviceClass.POWER, 639 | ), 640 | ) 641 | 642 | 643 | def async_remove_old_sub_device(self): 644 | """删除旧的子设备""" 645 | 646 | # 获取设备注册表 647 | device_reg = dr.async_get(self) 648 | 649 | prefixes = SubSensorname.prefixes 650 | # 通过唯一标识符查找设备 651 | # 注意:这里假设你用 unique_id 来匹配设备 652 | device = None 653 | deviceid = [] 654 | for dev in device_reg.devices.values(): 655 | if not dev.identifiers: 656 | continue # 跳过没有标识符的设备 657 | # 遍历该设备的所有标识符 658 | for identifier in dev.identifiers: 659 | if len(identifier) < 2: 660 | continue # 跳过格式不正确的标识符 661 | # 假设你的设备标识符元组格式为 (domain, unique_id) 662 | if isinstance(identifier[1], (str, list)): # 确保可迭代 663 | if [prefix for prefix in prefixes if prefix in identifier[1]]: 664 | device = dev 665 | deviceid.append(device.id) 666 | 667 | for oldsub in deviceid: 668 | device_reg.async_remove_device(oldsub) 669 | _LOGGER.debug("Old sub device %s removed successfully", oldsub) 670 | 671 | 672 | async def async_setup_entry( 673 | hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback 674 | ) -> None: 675 | """Set up sensor entities based on a config entry.""" 676 | async_remove_old_sub_device(hass) 677 | 678 | coordinator = hass.data[DOMAIN][entry.entry_id] 679 | async_add_entities( 680 | MainDevEcowittSensor(coordinator, entry.unique_id, desc) 681 | for desc in SENSOR_DESCRIPTIONS 682 | if desc.key in coordinator.data 683 | ) 684 | # Subdevice Data 685 | subsensors: list[SubDevEcowittSensor] = [] 686 | for key in coordinator.data: 687 | if key in MultiSensorInfo.SENSOR_INFO: 688 | if key in MultiSensorInfo.SENSOR_INFO and MultiSensorInfo.SENSOR_INFO[key][ 689 | "data_type" 690 | ] in (WittiotDataTypes.LEAK, WittiotDataTypes.BATTERY_BINARY): 691 | continue 692 | mapping = ECOWITT_SENSORS_MAPPING[ 693 | MultiSensorInfo.SENSOR_INFO[key]["data_type"] 694 | ] 695 | description = dataclasses.replace( 696 | mapping, 697 | key=key, 698 | name=MultiSensorInfo.SENSOR_INFO[key]["name"], 699 | ) 700 | subsensors.append( 701 | SubDevEcowittSensor( 702 | coordinator, 703 | entry.unique_id, 704 | MultiSensorInfo.SENSOR_INFO[key]["dev_type"], 705 | description, 706 | ) 707 | ) 708 | async_add_entities(subsensors) 709 | 710 | if "iot_list" in coordinator.data: 711 | iot_sensors: list[IotDeviceSensor] = [] 712 | desc_map = {desc.key: desc for desc in IOT_SENSOR_DESCRIPTIONS} 713 | iot_data = coordinator.data["iot_list"] 714 | commands = iot_data["command"] 715 | for i, item in enumerate(commands): 716 | nickname = item.get("nickname") 717 | if nickname is None: 718 | continue 719 | for key in list(item): 720 | if key in desc_map: 721 | desc = desc_map[key] 722 | device_desc = dataclasses.replace( 723 | desc, 724 | key=f"{nickname}_{desc.key}", 725 | # name=f"{device_info.get('name', f'设备 {device_id}')} {desc.name}", 726 | ) 727 | # 添加到实体列表 728 | iot_sensors.append( 729 | IotDeviceSensor( 730 | coordinator=coordinator, 731 | device_id=nickname, 732 | description=device_desc, 733 | unique_id=entry.unique_id, 734 | ) 735 | ) 736 | # _LOGGER.info("%s : %s : %s", key, item[key], item["nickname"]) 737 | async_add_entities(iot_sensors) 738 | 739 | 740 | class MainDevEcowittSensor( 741 | CoordinatorEntity[EcowittDataUpdateCoordinator], SensorEntity 742 | ): 743 | """Define a Local sensor.""" 744 | 745 | _attr_has_entity_name = True 746 | entity_description: SensorEntityDescription 747 | 748 | def __init__( 749 | self, 750 | coordinator: EcowittDataUpdateCoordinator, 751 | device_name: str, 752 | description: SensorEntityDescription, 753 | ) -> None: 754 | """Initialize.""" 755 | super().__init__(coordinator) 756 | self._attr_device_info = DeviceInfo( 757 | identifiers={(DOMAIN, f"{device_name}")}, 758 | manufacturer="Ecowitt", 759 | name=f"{device_name}", 760 | model=coordinator.data["ver"], 761 | configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", 762 | ) 763 | self._attr_unique_id = f"{device_name}_{description.key}" 764 | self.entity_description = description 765 | 766 | @property 767 | def native_value(self) -> str | int | float | None: 768 | """Return the state.""" 769 | return self.coordinator.data.get(self.entity_description.key) 770 | 771 | 772 | class SubDevEcowittSensor( 773 | CoordinatorEntity[EcowittDataUpdateCoordinator], SensorEntity 774 | ): 775 | """Define an Local sensor.""" 776 | 777 | _attr_has_entity_name = True 778 | entity_description: SensorEntityDescription 779 | 780 | def __init__( 781 | self, 782 | coordinator: EcowittDataUpdateCoordinator, 783 | device_name: str, 784 | sensor_type: str, 785 | description: SensorEntityDescription, 786 | ) -> None: 787 | """Initialize.""" 788 | super().__init__(coordinator) 789 | # self._attr_device_info = DeviceInfo( 790 | # identifiers={(DOMAIN, f"{device_name}_{sensor_type}")}, 791 | # manufacturer="Ecowitt", 792 | # name=f"{sensor_type}", 793 | # model=coordinator.data["ver"], 794 | # configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", 795 | # via_device=(DOMAIN, f"{device_name}"), 796 | # ) 797 | self._attr_device_info = DeviceInfo( 798 | identifiers={(DOMAIN, f"{device_name}")}, 799 | manufacturer="Ecowitt", 800 | name=f"{device_name}", 801 | model=coordinator.data["ver"], 802 | configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", 803 | ) 804 | self._attr_unique_id = f"{device_name}_{description.key}" 805 | self.entity_description = description 806 | 807 | @property 808 | def native_value(self) -> str | int | float | None: 809 | """Return the state.""" 810 | return self.coordinator.data.get(self.entity_description.key) 811 | 812 | 813 | class IotDeviceSensor(CoordinatorEntity, SensorEntity): 814 | """表示 IoT 设备的传感器实体""" 815 | 816 | _attr_has_entity_name = True 817 | 818 | def __init__( 819 | self, 820 | coordinator: EcowittDataUpdateCoordinator, 821 | device_id: str, 822 | description: SensorEntityDescription, 823 | unique_id: str, 824 | ) -> None: 825 | """初始化 IoT 设备传感器""" 826 | super().__init__(coordinator) 827 | self.device_id = device_id 828 | self.entity_description = description 829 | self._attr_unique_id = f"{device_id}_{description.key}" 830 | 831 | # 设置设备信息 832 | self._attr_device_info = DeviceInfo( 833 | identifiers={(DOMAIN, f"{device_id}")}, 834 | name=f"{device_id}", 835 | manufacturer="Ecowitt", 836 | model=coordinator.data["ver"], 837 | configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", 838 | via_device=(DOMAIN, unique_id), 839 | ) 840 | 841 | @property 842 | def native_value(self) -> str | int | float | None: 843 | """获取传感器值""" 844 | # # 从协调器获取设备数据 845 | if "iot_list" in self.coordinator.data: 846 | iot_data = self.coordinator.data["iot_list"] 847 | commands = iot_data["command"] 848 | for i, item in enumerate(commands): 849 | nickname = item.get("nickname") 850 | if nickname is None: 851 | continue 852 | if nickname == self.device_id: 853 | key = self.entity_description.key.split("_", 1)[1] 854 | return item.get(key, None) 855 | return None 856 | -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "host": "[%key:common::config_flow::data::host%]" 7 | }, 8 | "data_description": { 9 | "host": "The IP address of the device." 10 | } 11 | } 12 | }, 13 | "error": { 14 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 15 | "no_devices": "You have connected to the device but have not received the returned data. Please check the device or restart the device." 16 | }, 17 | "abort": { 18 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 19 | } 20 | }, 21 | "entity": { 22 | "sensor": { 23 | "tempinf": { 24 | "name": "Indoor temperature" 25 | }, 26 | "humidityin": { 27 | "name": "Indoor humidity" 28 | }, 29 | "tempf": { 30 | "name": "Outdoor temperature" 31 | }, 32 | "baromrelin": { 33 | "name": "Relative pressure" 34 | }, 35 | "baromabsin": { 36 | "name": "Absolute pressure" 37 | }, 38 | "humidity": { 39 | "name": "Outdoor humidity" 40 | }, 41 | "winddir": { 42 | "name": "Wind direction" 43 | }, 44 | "windspeedmph": { 45 | "name": "Wind speed" 46 | }, 47 | "windgustmph": { 48 | "name": "Wind gust" 49 | }, 50 | "solarradiation": { 51 | "name": "Solar irradiance" 52 | }, 53 | "uv": { 54 | "name": "UV-Index" 55 | }, 56 | "daywindmax": { 57 | "name": "Day wind max" 58 | }, 59 | "feellike": { 60 | "name": "Feel likes" 61 | }, 62 | "dewpoint": { 63 | "name": "Dew point" 64 | }, 65 | "rainratein": { 66 | "name": "Rain rate" 67 | }, 68 | "eventrainin": { 69 | "name": "Rain event" 70 | }, 71 | "dailyrainin": { 72 | "name": "Daily rainfall" 73 | }, 74 | "weeklyrainin": { 75 | "name": "Weekly rainfall" 76 | }, 77 | "monthlyrainin": { 78 | "name": "Monthly rainfall" 79 | }, 80 | "yearlyrainin": { 81 | "name": "Yearly rainfall" 82 | }, 83 | "rrain_piezo": { 84 | "name": "Piezo rain rate" 85 | }, 86 | "erain_piezo": { 87 | "name": "Piezo rain event" 88 | }, 89 | "drain_piezo": { 90 | "name": "Daily piezo rainfall" 91 | }, 92 | "wrain_piezo": { 93 | "name": "Weekly piezo rainfall" 94 | }, 95 | "mrain_piezo": { 96 | "name": "Monthly piezo rainfall" 97 | }, 98 | "yrain_piezo": { 99 | "name": "Yearly piezo rainfall" 100 | }, 101 | "co2in": { 102 | "name": "Indoor co2" 103 | }, 104 | "co2in_24h": { 105 | "name": "Indoor 24H avg co2" 106 | }, 107 | "co2": { 108 | "name": "CO2" 109 | }, 110 | "co2_24h": { 111 | "name": "24H avg co2" 112 | }, 113 | "pm25_co2": { 114 | "name": "CO2 PM2.5" 115 | }, 116 | "pm25_24h_co2": { 117 | "name": "CO2 24H PM2.5 AQI" 118 | }, 119 | "pm10_co2": { 120 | "name": "CO2 PM10" 121 | }, 122 | "pm10_24h_co2": { 123 | "name": "CO2 24H PM10 AQI" 124 | }, 125 | "pm10_aqi_co2": { 126 | "name": "CO2 PM10 AQI" 127 | }, 128 | "pm25_aqi_co2": { 129 | "name": "CO2 PM2.5 AQI" 130 | }, 131 | "tf_co2": { 132 | "name": "CO2 temperature" 133 | }, 134 | "humi_co2": { 135 | "name": "CO2 humidity" 136 | }, 137 | "lightning": { 138 | "name": "Thunder last distance" 139 | }, 140 | "lightning_time": { 141 | "name": "Thunder last timestamp" 142 | }, 143 | "lightning_num": { 144 | "name": "Thunder daily count" 145 | }, 146 | "winddir10": { 147 | "name": "10Min.Avg Wind Direction" 148 | }, 149 | "apparent": { 150 | "name": "Apparent" 151 | }, 152 | "vpd": { 153 | "name": "VPD" 154 | }, 155 | "totalrainin": { 156 | "name": "Total rainfall" 157 | }, 158 | "24hrainin": { 159 | "name": "24 Hours rainfall" 160 | }, 161 | "train_piezo": { 162 | "name": "Total piezo rainfall" 163 | }, 164 | "24hrain_piezo": { 165 | "name": "24 Hours piezo rainfall" 166 | }, 167 | "con_batt": { 168 | "name": "Console Battery" 169 | }, 170 | "con_batt_volt": { 171 | "name": "Console Battery Voltage" 172 | }, 173 | "con_ext_volt": { 174 | "name": "Console External Power Voltage" 175 | }, 176 | "piezora_batt": { 177 | "name": "Piezo Battery" 178 | }, 179 | "pm1_co2": { 180 | "name": "CO2 PM1" 181 | }, 182 | "pm1_24h_co2": { 183 | "name": "CO2 24H PM1 AQI" 184 | }, 185 | "pm1_aqi_co2": { 186 | "name": "CO2 PM1 AQI" 187 | }, 188 | "pm4_co2": { 189 | "name": "CO2 PM4" 190 | }, 191 | "pm4_24h_co2": { 192 | "name": "CO2 24H PM4 AQI" 193 | }, 194 | "pm4_aqi_co2": { 195 | "name": "CO2 PM4 AQI" 196 | }, 197 | "pm1_24h_co2_add": { 198 | "name": "CO2 24H Avg PM1" 199 | }, 200 | "pm4_24h_co2_add": { 201 | "name": "CO2 24H Avg PM4" 202 | }, 203 | "pm25_24h_co2_add": { 204 | "name": "CO2 24H Avg PM2.5" 205 | }, 206 | "pm10_24h_co2_add": { 207 | "name": "CO2 24H Avg PM10" 208 | }, 209 | "iotbatt": { 210 | "name": "Battery" 211 | }, 212 | "signal": { 213 | "name": "Signal" 214 | }, 215 | "run_time": { 216 | "name": "Run time" 217 | }, 218 | "ver": { 219 | "name": "Version" 220 | }, 221 | "wfc02_position": { 222 | "name": "Valve Opening Percentage" 223 | }, 224 | "wfc02_flow_velocity": { 225 | "name": "Flow velocity" 226 | }, 227 | "velocity_total": { 228 | "name": "Total" 229 | }, 230 | "flow_velocity": { 231 | "name": "Flow velocity" 232 | }, 233 | "data_water_t": { 234 | "name": "Temperature" 235 | }, 236 | "data_ac_v": { 237 | "name": "Voltage" 238 | }, 239 | "elect_total": { 240 | "name": "Energy" 241 | }, 242 | "realtime_power": { 243 | "name": "Power" 244 | } 245 | }, 246 | "binary_sensor": { 247 | "srain_piezo": { 248 | "name": "Rain Status" 249 | }, 250 | "rfnet_state": { 251 | "name": "Is online" 252 | }, 253 | "iot_running": { 254 | "name": "Running" 255 | } 256 | } 257 | } 258 | } -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/switch.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from wittiot import API 4 | import asyncio 5 | import dataclasses 6 | from homeassistant.core import HomeAssistant 7 | from homeassistant.config_entries import ConfigEntry 8 | from homeassistant.helpers.device_registry import DeviceInfo 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 10 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 11 | from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription 12 | from homeassistant.const import EntityCategory 13 | from homeassistant.helpers.event import async_call_later 14 | from homeassistant.const import ( 15 | CONF_HOST, 16 | ) 17 | from .const import DOMAIN 18 | from .coordinator import EcowittDataUpdateCoordinator 19 | 20 | _LOGGER = logging.getLogger(__name__) 21 | 22 | SWITCH_DESCRIPTIONS = ( 23 | SwitchEntityDescription( 24 | key="iot_running", 25 | entity_category=EntityCategory.CONFIG, 26 | ), 27 | ) 28 | 29 | 30 | async def async_setup_entry( 31 | hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback 32 | ) -> None: 33 | """设置开关平台.""" 34 | coordinator = hass.data[DOMAIN][entry.entry_id] 35 | 36 | # 为每个设备创建开关实体 37 | switches = [] 38 | if "iot_list" in coordinator.data: 39 | iot_data = coordinator.data["iot_list"] 40 | commands = iot_data["command"] 41 | desc_map = {desc.key: desc for desc in SWITCH_DESCRIPTIONS} 42 | for i, item in enumerate(commands): 43 | rfnet_state = item.get("rfnet_state") 44 | if rfnet_state == 0: 45 | continue 46 | for key in list(item): 47 | if key in desc_map: 48 | desc = desc_map[key] 49 | device_desc = dataclasses.replace( 50 | desc, 51 | key=f"{item.get('nickname')}_{desc.key}", 52 | ) 53 | switches.append( 54 | EcowittSwitch( 55 | coordinator=coordinator, 56 | device_id=item.get("nickname"), 57 | description=device_desc, 58 | unique_id=entry.unique_id, 59 | ) 60 | ) 61 | async_add_entities(switches) 62 | return True 63 | 64 | 65 | class EcowittSwitch(CoordinatorEntity, SwitchEntity): 66 | """表示Ecowitt设备的开关实体.""" 67 | 68 | def __init__( 69 | self, 70 | coordinator: EcowittDataUpdateCoordinator, 71 | device_id: str, 72 | description: SwitchEntityDescription, 73 | unique_id: str, 74 | ) -> None: 75 | """表示Ecowitt设备的开关实体.""" 76 | super().__init__(coordinator) 77 | self.device_id = device_id 78 | self.entity_description = description 79 | self._attr_unique_id = f"{device_id}_{description.key}" 80 | 81 | self._pending_state = None # 跟踪待确认的状态 82 | self._pending_timestamp = None # 记录状态改变的时间 83 | self._timeout_handle = None # 超时处理句柄 84 | # 设置设备信息 85 | self._attr_device_info = DeviceInfo( 86 | identifiers={(DOMAIN, f"{device_id}")}, 87 | name=f"{device_id}", 88 | manufacturer="Ecowitt", 89 | model=coordinator.data["ver"], 90 | configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", 91 | via_device=(DOMAIN, unique_id), 92 | ) 93 | 94 | if "iot_list" in self.coordinator.data: 95 | iot_data = self.coordinator.data["iot_list"] 96 | commands = iot_data["command"] 97 | for i, item in enumerate(commands): 98 | nickname = item.get("nickname") 99 | rfnet_state = item.get("rfnet_state") 100 | if rfnet_state == 0: 101 | continue 102 | if nickname == self.device_id: 103 | # key = self.entity_description.key.split("_", 1)[1] 104 | self._iot_id = item.get("id") 105 | self._iot_model = item.get("model") 106 | # self._iot_is_on = item.get("iot_running") 107 | 108 | @property 109 | def is_on(self) -> bool: 110 | """从协调器获取设备数据.""" 111 | return self._get_actual_state() # 如果数据不可用返回None 112 | 113 | async def async_turn_on(self, **kwargs): 114 | """打开设备.""" 115 | await self._async_set_state(True) 116 | 117 | async def async_turn_off(self, **kwargs): 118 | """关闭设备.""" 119 | await self._async_set_state(False) 120 | 121 | def _get_actual_state(self) -> bool: 122 | """从协调器获取实际设备状态.""" 123 | if "iot_list" in self.coordinator.data: 124 | iot_data = self.coordinator.data["iot_list"] 125 | commands = iot_data["command"] 126 | for i, item in enumerate(commands): 127 | nickname = item.get("nickname") 128 | rfnet_state = item.get("rfnet_state") 129 | if rfnet_state == 0: 130 | continue 131 | if nickname == self.device_id: 132 | # key = self.entity_description.key.split("_", 1)[1] 133 | return bool(item.get("iot_running", 0)) 134 | return False # 默认返回关状态 135 | 136 | async def _async_set_state(self, state: bool): 137 | """设置设备状态(带待处理状态管理)""" 138 | # 取消之前的超时检查 139 | if self._timeout_handle: 140 | self._timeout_handle() 141 | self._timeout_handle = None 142 | 143 | # 设置待处理状态 144 | self._pending_state = state 145 | self._pending_timestamp = time.time() 146 | 147 | # 乐观更新:立即改变UI状态 148 | self._attr_is_on = state 149 | self.async_write_ha_state() 150 | 151 | # 发送控制命令 152 | state_value = 1 if state else 0 153 | await self.coordinator.api.switch_iotdevice( 154 | self._iot_id, self._iot_model, state_value 155 | ) 156 | 157 | # 设置状态验证超时(5秒后检查实际状态) 158 | self._timeout_handle = async_call_later( 159 | self.hass, 160 | 5, # 7秒后验证状态 161 | self._async_verify_state, 162 | ) 163 | 164 | # 延迟1秒后请求协调器更新数据 165 | await self.coordinator.async_request_refresh() 166 | 167 | async def _async_verify_state(self, _): 168 | """验证设备实际状态是否与预期一致""" 169 | self._timeout_handle = None 170 | 171 | # 获取当前实际状态 172 | current_state = self._get_actual_state() 173 | 174 | if self._pending_state == current_state: 175 | # 状态匹配,清除待处理标志 176 | self._pending_state = None 177 | self._pending_timestamp = None 178 | else: 179 | # 状态不匹配,恢复实际状态 180 | self._attr_is_on = current_state 181 | self._pending_state = None 182 | self._pending_timestamp = None 183 | self.async_write_ha_state() 184 | # _LOGGER.warning( 185 | # "设备状态未按预期改变。预期: %s, 实际: %s", 186 | # "开" if self._pending_state else "关", 187 | # "开" if current_state else "关", 188 | # ) 189 | -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Gerät schon konfiguriert" 5 | }, 6 | "error": { 7 | "cannot_connect": "Verbindungs Fehler", 8 | "invalid_auth": "Ungültige Authentifizierung", 9 | "unknown": "Unerwarteter Fehler" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "host": "IP Adresse Gerät" 15 | }, 16 | "description": "Bitte geben Sie die IP-Adresse des Gerätes an zum Darstellen der Daten", 17 | "title": "Gerät Konfiguration" 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "title": "Gerät Konfiguration", 25 | "description": "Bitte geben Sie die IP-Adresse des Gerätes an zum Darstellen der Daten", 26 | "data": { 27 | "host": "IP Adresse Gerät" 28 | } 29 | } 30 | } 31 | }, 32 | "entity": { 33 | "sensor": { 34 | "tempinf": { 35 | "name": "Innen Temperatur" 36 | }, 37 | "humidityin": { 38 | "name": "Innen Luftfeuchte" 39 | }, 40 | "tempf": { 41 | "name": "Aussen Temperatur" 42 | }, 43 | "baromrelin": { 44 | "name": "Luftdruck" 45 | }, 46 | "baromabsin": { 47 | "name": "Luftdruck abs." 48 | }, 49 | "humidity": { 50 | "name": "Aussen Luftfeuchte" 51 | }, 52 | "winddir": { 53 | "name": "Windrichtung" 54 | }, 55 | "windspeedmph": { 56 | "name": "Windgeschwindigkeit" 57 | }, 58 | "windgustmph": { 59 | "name": "Windböen" 60 | }, 61 | "solarradiation": { 62 | "name": "Solarstrahlung" 63 | }, 64 | "uv": { 65 | "name": "UV-Index" 66 | }, 67 | "daywindmax": { 68 | "name": "Wind max Tag" 69 | }, 70 | "feellike": { 71 | "name": "Gefühlte Temperatur" 72 | }, 73 | "apparent": { 74 | "name": "Apparent Temperatur" 75 | }, 76 | "dewpoint": { 77 | "name": "Taupunkt" 78 | }, 79 | "lds_air_ch1": { 80 | "name": "Höhenmesser 1 gesamt" 81 | }, 82 | "lds_heat_ch1": { 83 | "name": "Höhenmesser 1 Heizung" 84 | }, 85 | "lds_dep_ch1": { 86 | "name": "Höhenmesser 1" 87 | }, 88 | "lds_air_ch2": { 89 | "name": "Höhenmesser 2 gesamt" 90 | }, 91 | "lds_heat_ch2": { 92 | "name": "Höhenmesser 2 Heizung" 93 | }, 94 | "lds_dep_ch2": { 95 | "name": "Höhenmesser 2" 96 | }, 97 | "lds_air_ch3": { 98 | "name": "Höhenmesser 3 gesamt" 99 | }, 100 | "lds_heat_ch3": { 101 | "name": "Höhenmesser 3 Heizung" 102 | }, 103 | "lds_dep_ch3": { 104 | "name": "Höhenmesser 3" 105 | }, 106 | "lds_air_ch4": { 107 | "name": "Höhenmesser 4 gesamt" 108 | }, 109 | "lds_heat_ch4": { 110 | "name": "Höhenmesser 4 Heizung" 111 | }, 112 | "lds_dep_ch4": { 113 | "name": "Höhenmesser 4" 114 | }, 115 | "rainratein": { 116 | "name": "Regenrate" 117 | }, 118 | "eventrainin": { 119 | "name": "Regensturm" 120 | }, 121 | "dailyrainin": { 122 | "name": "Regen Tag" 123 | }, 124 | "weeklyrainin": { 125 | "name": "Regen Woche" 126 | }, 127 | "monthlyrainin": { 128 | "name": "Regen Monat" 129 | }, 130 | "yearlyrainin": { 131 | "name": "Regen Jahr" 132 | }, 133 | "rrain_piezo": { 134 | "name": "Regenrate Piezo" 135 | }, 136 | "erain_piezo": { 137 | "name": "Regensturm Piezo" 138 | }, 139 | "drain_piezo": { 140 | "name": "Regen Tag Piezo" 141 | }, 142 | "wrain_piezo": { 143 | "name": "Regen Woche Piezo" 144 | }, 145 | "mrain_piezo": { 146 | "name": "Regen Monat Piezo" 147 | }, 148 | "yrain_piezo": { 149 | "name": "Regen Jahr Piezo" 150 | }, 151 | "co2in": { 152 | "name": "CO2 Innen" 153 | }, 154 | "co2in_24h": { 155 | "name": "CO2 Innen 24H durchs." 156 | }, 157 | "co2": { 158 | "name": "CO2" 159 | }, 160 | "co2_24h": { 161 | "name": "CO2 24H durchs." 162 | }, 163 | "pm25_co2": { 164 | "name": "CO2 PM2.5" 165 | }, 166 | "pm25_24h_co2": { 167 | "name": "CO2 PM2.5 AQI 24H" 168 | }, 169 | "pm10_co2": { 170 | "name": "CO2 PM10" 171 | }, 172 | "pm10_24h_co2": { 173 | "name": "CO2 PM10 AQI 24H" 174 | }, 175 | "pm10_aqi_co2": { 176 | "name": "CO2 PM10 AQI" 177 | }, 178 | "pm25_aqi_co2": { 179 | "name": "CO2 PM2.5 AQI" 180 | }, 181 | "tf_co2": { 182 | "name": "CO2 Temperatur" 183 | }, 184 | "humi_co2": { 185 | "name": "CO2 Luftfeuchte" 186 | }, 187 | "lightning": { 188 | "name": "Blitze letzte Entfernung" 189 | }, 190 | "lightning_time": { 191 | "name": "Blitze Datum" 192 | }, 193 | "lightning_num": { 194 | "name": "Blitze Anzahl" 195 | }, 196 | "winddir10": { 197 | "name": "Windrichtung 10min durchs." 198 | }, 199 | "vpd": { 200 | "name": "Dampfdruckdefizit" 201 | }, 202 | "totalrainin": { 203 | "name": "Regen Gesamt" 204 | }, 205 | "24hrainin": { 206 | "name": "Regen 24 Std" 207 | }, 208 | "train_piezo": { 209 | "name": "Regen Gesamt Piezo" 210 | }, 211 | "24hrain_piezo": { 212 | "name": "Regen 24 Std. Piezo" 213 | }, 214 | "con_batt": { 215 | "name": "Konsole Batterie" 216 | }, 217 | "con_batt_volt": { 218 | "name": "Konsole Batterie Spannung" 219 | }, 220 | "con_ext_volt": { 221 | "name": "Konsole ext. Spannung" 222 | }, 223 | "srain_piezo": { 224 | "name": "Regen Status" 225 | }, 226 | "piezora_batt": { 227 | "name": "Batterie Piezo" 228 | }, 229 | "pm1_co2": { 230 | "name": "CO2 PM1" 231 | }, 232 | "pm1_24h_co2": { 233 | "name": "CO2 PM1 AQI 24H" 234 | }, 235 | "pm1_aqi_co2": { 236 | "name": "CO2 PM1 AQI" 237 | }, 238 | "pm4_co2": { 239 | "name": "CO2 PM4" 240 | }, 241 | "pm4_24h_co2": { 242 | "name": "CO2 PM4 AQI 24H" 243 | }, 244 | "pm4_aqi_co2": { 245 | "name": "CO2 PM4 AQI" 246 | }, 247 | "pm1_24h_co2_add": { 248 | "name": "CO2 Avg PM1 24H" 249 | }, 250 | "pm4_24h_co2_add": { 251 | "name": "CO2 Avg PM4 24H" 252 | }, 253 | "pm25_24h_co2_add": { 254 | "name": "CO2 Avg PM2.5 24H" 255 | }, 256 | "pm10_24h_co2_add": { 257 | "name": "CO2 Avg PM10 24H" 258 | }, 259 | "iotbatt": { 260 | "name": "Batterie" 261 | }, 262 | "signal": { 263 | "name": "Signal" 264 | }, 265 | "run_time": { 266 | "name": "Laufzeit" 267 | }, 268 | "ver": { 269 | "name": "Versionen" 270 | }, 271 | "wfc02_position": { 272 | "name": "Öffnungsprozentsatz" 273 | }, 274 | "wfc02_flow_velocity": { 275 | "name": "Fluss Geschwindigkeit" 276 | }, 277 | "velocity_total": { 278 | "name": "Gesamt" 279 | }, 280 | "flow_velocity": { 281 | "name": "Fluss Geschwindigkeit" 282 | }, 283 | "data_water_t": { 284 | "name": "Wasser Temperatur" 285 | }, 286 | "data_ac_v": { 287 | "name": "Spannung" 288 | }, 289 | "elect_total": { 290 | "name": "Energie" 291 | }, 292 | "realtime_power": { 293 | "name": "Leistung" 294 | } 295 | }, 296 | "binary_sensor": { 297 | "srain_piezo": { 298 | "name": "Regen Status" 299 | }, 300 | "rfnet_state": { 301 | "name": "Ist online" 302 | }, 303 | "iot_running": { 304 | "name": "Running" 305 | } 306 | } 307 | } 308 | } -------------------------------------------------------------------------------- /custom_components/ha_ecowitt_iot/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect", 8 | "invalid_auth": "Invalid authentication", 9 | "unknown": "Unexpected error" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "host": "Device IP address" 15 | }, 16 | "description": "Please enter the IP address of the device to view the data", 17 | "title": "Device configuration" 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "title": "Configure", 25 | "description": "Update the settings for your Ecowitt device", 26 | "data": { 27 | "host": "Device IP address" 28 | } 29 | } 30 | } 31 | }, 32 | "entity": { 33 | "sensor": { 34 | "tempinf": { 35 | "name": "Indoor temperature" 36 | }, 37 | "humidityin": { 38 | "name": "Indoor humidity" 39 | }, 40 | "tempf": { 41 | "name": "Outdoor temperature" 42 | }, 43 | "baromrelin": { 44 | "name": "Relative pressure" 45 | }, 46 | "baromabsin": { 47 | "name": "Absolute pressure" 48 | }, 49 | "humidity": { 50 | "name": "Outdoor humidity" 51 | }, 52 | "winddir": { 53 | "name": "Wind direction" 54 | }, 55 | "windspeedmph": { 56 | "name": "Wind speed" 57 | }, 58 | "windgustmph": { 59 | "name": "Wind gust" 60 | }, 61 | "solarradiation": { 62 | "name": "Solar irradiance" 63 | }, 64 | "uv": { 65 | "name": "UV-Index" 66 | }, 67 | "daywindmax": { 68 | "name": "Day wind max" 69 | }, 70 | "feellike": { 71 | "name": "Feel likes" 72 | }, 73 | "dewpoint": { 74 | "name": "Dew point" 75 | }, 76 | "rainratein": { 77 | "name": "Rain rate" 78 | }, 79 | "eventrainin": { 80 | "name": "Rain event" 81 | }, 82 | "dailyrainin": { 83 | "name": "Daily rainfall" 84 | }, 85 | "weeklyrainin": { 86 | "name": "Weekly rainfall" 87 | }, 88 | "monthlyrainin": { 89 | "name": "Monthly rainfall" 90 | }, 91 | "yearlyrainin": { 92 | "name": "Yearly rainfall" 93 | }, 94 | "rrain_piezo": { 95 | "name": "Piezo rain rate" 96 | }, 97 | "erain_piezo": { 98 | "name": "Piezo rain event" 99 | }, 100 | "drain_piezo": { 101 | "name": "Daily piezo rainfall" 102 | }, 103 | "wrain_piezo": { 104 | "name": "Weekly piezo rainfall" 105 | }, 106 | "mrain_piezo": { 107 | "name": "Monthly piezo rainfall" 108 | }, 109 | "yrain_piezo": { 110 | "name": "Yearly piezo rainfall" 111 | }, 112 | "co2in": { 113 | "name": "Indoor co2" 114 | }, 115 | "co2in_24h": { 116 | "name": "Indoor 24H avg co2" 117 | }, 118 | "co2": { 119 | "name": "CO2" 120 | }, 121 | "co2_24h": { 122 | "name": "24H avg co2" 123 | }, 124 | "pm25_co2": { 125 | "name": "CO2 PM2.5" 126 | }, 127 | "pm25_24h_co2": { 128 | "name": "CO2 24H PM2.5 AQI" 129 | }, 130 | "pm10_co2": { 131 | "name": "CO2 PM10" 132 | }, 133 | "pm10_24h_co2": { 134 | "name": "CO2 24H PM10 AQI" 135 | }, 136 | "pm10_aqi_co2": { 137 | "name": "CO2 PM10 AQI" 138 | }, 139 | "pm25_aqi_co2": { 140 | "name": "CO2 PM2.5 AQI" 141 | }, 142 | "tf_co2": { 143 | "name": "CO2 temperature" 144 | }, 145 | "humi_co2": { 146 | "name": "CO2 humidity" 147 | }, 148 | "lightning": { 149 | "name": "Thunder last distance" 150 | }, 151 | "lightning_time": { 152 | "name": "Thunder last timestamp" 153 | }, 154 | "lightning_num": { 155 | "name": "Thunder daily count" 156 | }, 157 | "winddir10": { 158 | "name": "10Min.Avg Wind Direction" 159 | }, 160 | "apparent": { 161 | "name": "Apparent Temperatur" 162 | }, 163 | "vpd": { 164 | "name": "VPD" 165 | }, 166 | "totalrainin": { 167 | "name": "Total rainfall" 168 | }, 169 | "24hrainin": { 170 | "name": "24 Hours rainfall" 171 | }, 172 | "train_piezo": { 173 | "name": "Total piezo rainfall" 174 | }, 175 | "24hrain_piezo": { 176 | "name": "24 Hours piezo rainfall" 177 | }, 178 | "con_batt": { 179 | "name": "Console Battery" 180 | }, 181 | "con_batt_volt": { 182 | "name": "Console Battery Voltage" 183 | }, 184 | "con_ext_volt": { 185 | "name": "Console External Power Voltage" 186 | }, 187 | "srain_piezo": { 188 | "name": "Rain Status" 189 | }, 190 | "piezora_batt": { 191 | "name": "Piezo Battery" 192 | }, 193 | "pm1_co2": { 194 | "name": "CO2 PM1" 195 | }, 196 | "pm1_24h_co2": { 197 | "name": "CO2 24H PM1 AQI" 198 | }, 199 | "pm1_aqi_co2": { 200 | "name": "CO2 PM1 AQI" 201 | }, 202 | "pm4_co2": { 203 | "name": "CO2 PM4" 204 | }, 205 | "pm4_24h_co2": { 206 | "name": "CO2 24H PM4 AQI" 207 | }, 208 | "pm4_aqi_co2": { 209 | "name": "CO2 PM4 AQI" 210 | }, 211 | "pm1_24h_co2_add": { 212 | "name": "CO2 24H Avg PM1" 213 | }, 214 | "pm4_24h_co2_add": { 215 | "name": "CO2 24H Avg PM4" 216 | }, 217 | "pm25_24h_co2_add": { 218 | "name": "CO2 24H Avg PM2.5" 219 | }, 220 | "pm10_24h_co2_add": { 221 | "name": "CO2 24H Avg PM10" 222 | }, 223 | "iotbatt": { 224 | "name": "Battery" 225 | }, 226 | "signal": { 227 | "name": "Signal" 228 | }, 229 | "run_time": { 230 | "name": "Run time" 231 | }, 232 | "ver": { 233 | "name": "Version" 234 | }, 235 | "wfc02_position": { 236 | "name": "Valve Opening Percentage" 237 | }, 238 | "wfc02_flow_velocity": { 239 | "name": "Flow velocity" 240 | }, 241 | "velocity_total": { 242 | "name": "Total" 243 | }, 244 | "flow_velocity": { 245 | "name": "Flow velocity" 246 | }, 247 | "data_water_t": { 248 | "name": "Temperature" 249 | }, 250 | "data_ac_v": { 251 | "name": "Voltage" 252 | }, 253 | "elect_total": { 254 | "name": "Energy" 255 | }, 256 | "realtime_power": { 257 | "name": "Power" 258 | } 259 | }, 260 | "binary_sensor": { 261 | "srain_piezo": { 262 | "name": "Rain Status" 263 | }, 264 | "rfnet_state": { 265 | "name": "Is online" 266 | }, 267 | "iot_running": { 268 | "name": "Running" 269 | } 270 | } 271 | } 272 | } -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ecowitt Official Integration", 3 | "render_readme": true, 4 | "homeassistant": "2023.11.0" 5 | } -------------------------------------------------------------------------------- /img/20240223161726.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecowitt/ha-ecowitt-iot/8fc212b6f1da081775ed50c5c399d80840bf9a76/img/20240223161726.png -------------------------------------------------------------------------------- /img/20240223161811.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecowitt/ha-ecowitt-iot/8fc212b6f1da081775ed50c5c399d80840bf9a76/img/20240223161811.png -------------------------------------------------------------------------------- /img/TF1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecowitt/ha-ecowitt-iot/8fc212b6f1da081775ed50c5c399d80840bf9a76/img/TF1.jpg -------------------------------------------------------------------------------- /img/TF2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecowitt/ha-ecowitt-iot/8fc212b6f1da081775ed50c5c399d80840bf9a76/img/TF2.jpg -------------------------------------------------------------------------------- /img/TF3-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecowitt/ha-ecowitt-iot/8fc212b6f1da081775ed50c5c399d80840bf9a76/img/TF3-3.jpg -------------------------------------------------------------------------------- /img/TF4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecowitt/ha-ecowitt-iot/8fc212b6f1da081775ed50c5c399d80840bf9a76/img/TF4.jpg --------------------------------------------------------------------------------