Ecowitt integration (#77441)
* Add ecowitt integration * add tests * use total * use total * test coverage * Update homeassistant/components/ecowitt/__init__.py Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Update homeassistant/components/ecowitt/binary_sensor.py Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Update homeassistant/components/ecowitt/entity.py Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Update homeassistant/components/ecowitt/diagnostics.py Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * add to async_on_unload * remove attr_name / unload callback * support unload platforms * using replace * address mapping * update type * mark final * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Fix bracket * Fix another bracket * Address comment * Add strings * update tests * Update homeassistant/components/ecowitt/strings.json Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * update text * Update homeassistant/components/ecowitt/strings.json Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
ee6ffb1be4
commit
105bb3e082
17 changed files with 705 additions and 1 deletions
|
@ -260,6 +260,11 @@ omit =
|
|||
homeassistant/components/econet/const.py
|
||||
homeassistant/components/econet/sensor.py
|
||||
homeassistant/components/econet/water_heater.py
|
||||
homeassistant/components/ecowitt/__init__.py
|
||||
homeassistant/components/ecowitt/binary_sensor.py
|
||||
homeassistant/components/ecowitt/diagnostics.py
|
||||
homeassistant/components/ecowitt/entity.py
|
||||
homeassistant/components/ecowitt/sensor.py
|
||||
homeassistant/components/ecovacs/*
|
||||
homeassistant/components/edl21/*
|
||||
homeassistant/components/eddystone_temperature/sensor.py
|
||||
|
|
|
@ -276,6 +276,8 @@ build.json @home-assistant/supervisor
|
|||
/homeassistant/components/econet/ @vangorra @w1ll1am23
|
||||
/tests/components/econet/ @vangorra @w1ll1am23
|
||||
/homeassistant/components/ecovacs/ @OverloadUT
|
||||
/homeassistant/components/ecowitt/ @pvizeli
|
||||
/tests/components/ecowitt/ @pvizeli
|
||||
/homeassistant/components/edl21/ @mtdcr
|
||||
/homeassistant/components/efergy/ @tkdrob
|
||||
/tests/components/efergy/ @tkdrob
|
||||
|
|
47
homeassistant/components/ecowitt/__init__.py
Normal file
47
homeassistant/components/ecowitt/__init__.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
"""The Ecowitt Weather Station Component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aioecowitt import EcoWittListener
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
|
||||
from .const import CONF_PATH, DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up the Ecowitt component from UI."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
ecowitt = hass.data[DOMAIN][entry.entry_id] = EcoWittListener(
|
||||
port=entry.data[CONF_PORT], path=entry.data[CONF_PATH]
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
await ecowitt.start()
|
||||
|
||||
# Close on shutdown
|
||||
async def _stop_ecowitt(_: Event):
|
||||
"""Stop the Ecowitt listener."""
|
||||
await ecowitt.stop()
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_ecowitt)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
ecowitt = hass.data[DOMAIN][entry.entry_id]
|
||||
await ecowitt.stop()
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
71
homeassistant/components/ecowitt/binary_sensor.py
Normal file
71
homeassistant/components/ecowitt/binary_sensor.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
"""Support for Ecowitt Weather Stations."""
|
||||
import dataclasses
|
||||
from typing import Final
|
||||
|
||||
from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import EcowittEntity
|
||||
|
||||
ECOWITT_BINARYSENSORS_MAPPING: Final = {
|
||||
EcoWittSensorTypes.LEAK: BinarySensorEntityDescription(
|
||||
key="LEAK", device_class=BinarySensorDeviceClass.MOISTURE
|
||||
),
|
||||
EcoWittSensorTypes.BATTERY_BINARY: BinarySensorEntityDescription(
|
||||
key="BATTERY", device_class=BinarySensorDeviceClass.BATTERY
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Add sensors if new."""
|
||||
ecowitt: EcoWittListener = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
def _new_sensor(sensor: EcoWittSensor) -> None:
|
||||
"""Add new sensor."""
|
||||
if sensor.stype not in ECOWITT_BINARYSENSORS_MAPPING:
|
||||
return
|
||||
mapping = ECOWITT_BINARYSENSORS_MAPPING[sensor.stype]
|
||||
|
||||
# Setup sensor description
|
||||
description = dataclasses.replace(
|
||||
mapping,
|
||||
key=sensor.key,
|
||||
name=sensor.name,
|
||||
)
|
||||
|
||||
async_add_entities([EcowittBinarySensorEntity(sensor, description)])
|
||||
|
||||
ecowitt.new_sensor_cb.append(_new_sensor)
|
||||
entry.async_on_unload(lambda: ecowitt.new_sensor_cb.remove(_new_sensor))
|
||||
|
||||
# Add all sensors that are already known
|
||||
for sensor in ecowitt.sensors.values():
|
||||
_new_sensor(sensor)
|
||||
|
||||
|
||||
class EcowittBinarySensorEntity(EcowittEntity, BinarySensorEntity):
|
||||
"""Representation of a Ecowitt BinarySensor."""
|
||||
|
||||
def __init__(
|
||||
self, sensor: EcoWittSensor, description: BinarySensorEntityDescription
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(sensor)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.ecowitt.value > 0
|
79
homeassistant/components/ecowitt/config_flow.py
Normal file
79
homeassistant/components/ecowitt/config_flow.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
"""Config flow for ecowitt."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
from aioecowitt import EcoWittListener
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import CONF_PATH, DEFAULT_PORT, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PATH, default=f"/{secrets.token_urlsafe(16)}"): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
|
||||
"""Validate user input."""
|
||||
# Check if the port is in use
|
||||
try:
|
||||
listener = EcoWittListener(port=data[CONF_PORT])
|
||||
await listener.start()
|
||||
await listener.stop()
|
||||
except OSError:
|
||||
raise InvalidPort from None
|
||||
|
||||
return {"title": f"Ecowitt on port {data[CONF_PORT]}"}
|
||||
|
||||
|
||||
class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for the Ecowitt."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
errors = {}
|
||||
|
||||
# Check if the port is in use by another config entry
|
||||
self._async_abort_entries_match({CONF_PORT: user_input[CONF_PORT]})
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except InvalidPort:
|
||||
errors["base"] = "invalid_port"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class InvalidPort(HomeAssistantError):
|
||||
"""Error to indicate there port is not usable."""
|
7
homeassistant/components/ecowitt/const.py
Normal file
7
homeassistant/components/ecowitt/const.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
"""Constants used by ecowitt component."""
|
||||
|
||||
DOMAIN = "ecowitt"
|
||||
|
||||
DEFAULT_PORT = 49199
|
||||
|
||||
CONF_PATH = "path"
|
39
homeassistant/components/ecowitt/diagnostics.py
Normal file
39
homeassistant/components/ecowitt/diagnostics.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
"""Provides diagnostics for EcoWitt."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioecowitt import EcoWittListener
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device entry."""
|
||||
ecowitt: EcoWittListener = hass.data[DOMAIN][entry.entry_id]
|
||||
station_id = next(item[1] for item in device.identifiers if item[0] == DOMAIN)
|
||||
|
||||
station = ecowitt.stations[station_id]
|
||||
|
||||
data = {
|
||||
"device": {
|
||||
"name": station.station,
|
||||
"model": station.model,
|
||||
"frequency": station.frequency,
|
||||
"version": station.version,
|
||||
},
|
||||
"raw": ecowitt.last_values[station_id],
|
||||
"sensors": {
|
||||
sensor.key: sensor.value
|
||||
for sensor in station.sensors
|
||||
if sensor.station.key == station_id
|
||||
},
|
||||
}
|
||||
|
||||
return data
|
46
homeassistant/components/ecowitt/entity.py
Normal file
46
homeassistant/components/ecowitt/entity.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
"""The Ecowitt Weather Station Entity."""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from aioecowitt import EcoWittSensor
|
||||
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class EcowittEntity(Entity):
|
||||
"""Base class for Ecowitt Weather Station."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, sensor: EcoWittSensor) -> None:
|
||||
"""Construct the entity."""
|
||||
self.ecowitt: EcoWittSensor = sensor
|
||||
|
||||
self._attr_unique_id = f"{sensor.station.key}-{sensor.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, sensor.station.key),
|
||||
},
|
||||
name=sensor.station.station,
|
||||
model=sensor.station.model,
|
||||
sw_version=sensor.station.version,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Install listener for updates later."""
|
||||
|
||||
def _update_state():
|
||||
"""Update the state on callback."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.ecowitt.update_cb.append(_update_state)
|
||||
self.async_on_remove(lambda: self.ecowitt.update_cb.remove(_update_state))
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return whether the state is based on actual reading from device."""
|
||||
return (self.ecowitt.last_update_m + 5 * 60) > time.monotonic()
|
9
homeassistant/components/ecowitt/manifest.json
Normal file
9
homeassistant/components/ecowitt/manifest.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"domain": "ecowitt",
|
||||
"name": "Ecowitt Weather Station",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
|
||||
"requirements": ["aioecowitt==2022.08.3"],
|
||||
"codeowners": ["@pvizeli"],
|
||||
"iot_class": "local_push"
|
||||
}
|
247
homeassistant/components/ecowitt/sensor.py
Normal file
247
homeassistant/components/ecowitt/sensor.py
Normal file
|
@ -0,0 +1,247 @@
|
|||
"""Support for Ecowitt Weather Stations."""
|
||||
import dataclasses
|
||||
from typing import Final
|
||||
|
||||
from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
AREA_SQUARE_METERS,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
DEGREE,
|
||||
ELECTRIC_POTENTIAL_VOLT,
|
||||
LENGTH_INCHES,
|
||||
LENGTH_KILOMETERS,
|
||||
LENGTH_MILES,
|
||||
LENGTH_MILLIMETERS,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
POWER_WATT,
|
||||
PRECIPITATION_INCHES_PER_HOUR,
|
||||
PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
PRESSURE_HPA,
|
||||
PRESSURE_INHG,
|
||||
SPEED_KILOMETERS_PER_HOUR,
|
||||
SPEED_MILES_PER_HOUR,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
UV_INDEX,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import EcowittEntity
|
||||
|
||||
_METRIC: Final = (
|
||||
EcoWittSensorTypes.TEMPERATURE_C,
|
||||
EcoWittSensorTypes.RAIN_COUNT_MM,
|
||||
EcoWittSensorTypes.RAIN_RATE_MM,
|
||||
EcoWittSensorTypes.LIGHTNING_DISTANCE_KM,
|
||||
EcoWittSensorTypes.SPEED_KPH,
|
||||
EcoWittSensorTypes.PRESSURE_HPA,
|
||||
)
|
||||
_IMPERIAL: Final = (
|
||||
EcoWittSensorTypes.TEMPERATURE_F,
|
||||
EcoWittSensorTypes.RAIN_COUNT_INCHES,
|
||||
EcoWittSensorTypes.RAIN_RATE_INCHES,
|
||||
EcoWittSensorTypes.LIGHTNING_DISTANCE_MILES,
|
||||
EcoWittSensorTypes.SPEED_MPH,
|
||||
EcoWittSensorTypes.PRESSURE_INHG,
|
||||
)
|
||||
|
||||
|
||||
ECOWITT_SENSORS_MAPPING: Final = {
|
||||
EcoWittSensorTypes.HUMIDITY: SensorEntityDescription(
|
||||
key="HUMIDITY",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.DEGREE: SensorEntityDescription(
|
||||
key="DEGREE", native_unit_of_measurement=DEGREE
|
||||
),
|
||||
EcoWittSensorTypes.WATT_METERS_SQUARED: SensorEntityDescription(
|
||||
key="WATT_METERS_SQUARED",
|
||||
native_unit_of_measurement=f"{POWER_WATT}/{AREA_SQUARE_METERS}",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.UV_INDEX: SensorEntityDescription(
|
||||
key="UV_INDEX",
|
||||
native_unit_of_measurement=UV_INDEX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.PM25: SensorEntityDescription(
|
||||
key="PM25",
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.PM10: SensorEntityDescription(
|
||||
key="PM10",
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.BATTERY_PERCENTAGE: SensorEntityDescription(
|
||||
key="BATTERY_PERCENTAGE",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.BATTERY_VOLTAGE: SensorEntityDescription(
|
||||
key="BATTERY_VOLTAGE",
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.CO2_PPM: SensorEntityDescription(
|
||||
key="CO2_PPM",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.LUX: SensorEntityDescription(
|
||||
key="LUX",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.TIMESTAMP: SensorEntityDescription(
|
||||
key="TIMESTAMP", device_class=SensorDeviceClass.TIMESTAMP
|
||||
),
|
||||
EcoWittSensorTypes.VOLTAGE: SensorEntityDescription(
|
||||
key="VOLTAGE",
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.LIGHTNING_COUNT: SensorEntityDescription(
|
||||
key="LIGHTNING_COUNT",
|
||||
native_unit_of_measurement="strikes",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
EcoWittSensorTypes.TEMPERATURE_C: SensorEntityDescription(
|
||||
key="TEMPERATURE_C",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.TEMPERATURE_F: SensorEntityDescription(
|
||||
key="TEMPERATURE_F",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.RAIN_COUNT_MM: SensorEntityDescription(
|
||||
key="RAIN_COUNT_MM",
|
||||
native_unit_of_measurement=LENGTH_MILLIMETERS,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
EcoWittSensorTypes.RAIN_COUNT_INCHES: SensorEntityDescription(
|
||||
key="RAIN_COUNT_INCHES",
|
||||
native_unit_of_measurement=LENGTH_INCHES,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
EcoWittSensorTypes.RAIN_RATE_MM: SensorEntityDescription(
|
||||
key="RAIN_RATE_MM",
|
||||
native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.RAIN_RATE_INCHES: SensorEntityDescription(
|
||||
key="RAIN_RATE_INCHES",
|
||||
native_unit_of_measurement=PRECIPITATION_INCHES_PER_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.LIGHTNING_DISTANCE_KM: SensorEntityDescription(
|
||||
key="LIGHTNING_DISTANCE_KM",
|
||||
native_unit_of_measurement=LENGTH_KILOMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.LIGHTNING_DISTANCE_MILES: SensorEntityDescription(
|
||||
key="LIGHTNING_DISTANCE_MILES",
|
||||
native_unit_of_measurement=LENGTH_MILES,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.SPEED_KPH: SensorEntityDescription(
|
||||
key="SPEED_KPH",
|
||||
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.SPEED_MPH: SensorEntityDescription(
|
||||
key="SPEED_MPH",
|
||||
native_unit_of_measurement=SPEED_MILES_PER_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.PRESSURE_HPA: SensorEntityDescription(
|
||||
key="PRESSURE_HPA",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=PRESSURE_HPA,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
EcoWittSensorTypes.PRESSURE_INHG: SensorEntityDescription(
|
||||
key="PRESSURE_INHG",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=PRESSURE_INHG,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Add sensors if new."""
|
||||
ecowitt: EcoWittListener = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
def _new_sensor(sensor: EcoWittSensor) -> None:
|
||||
"""Add new sensor."""
|
||||
if sensor.stype not in ECOWITT_SENSORS_MAPPING:
|
||||
return
|
||||
|
||||
# Ignore metrics that are not supported by the user's locale
|
||||
if sensor.stype in _METRIC and not hass.config.units.is_metric:
|
||||
return
|
||||
if sensor.stype in _IMPERIAL and hass.config.units.is_metric:
|
||||
return
|
||||
mapping = ECOWITT_SENSORS_MAPPING[sensor.stype]
|
||||
|
||||
# Setup sensor description
|
||||
description = dataclasses.replace(
|
||||
mapping,
|
||||
key=sensor.key,
|
||||
name=sensor.name,
|
||||
)
|
||||
|
||||
async_add_entities([EcowittSensorEntity(sensor, description)])
|
||||
|
||||
ecowitt.new_sensor_cb.append(_new_sensor)
|
||||
entry.async_on_unload(lambda: ecowitt.new_sensor_cb.remove(_new_sensor))
|
||||
|
||||
# Add all sensors that are already known
|
||||
for sensor in ecowitt.sensors.values():
|
||||
_new_sensor(sensor)
|
||||
|
||||
|
||||
class EcowittSensorEntity(EcowittEntity, SensorEntity):
|
||||
"""Representation of a Ecowitt Sensor."""
|
||||
|
||||
def __init__(
|
||||
self, sensor: EcoWittSensor, description: SensorEntityDescription
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(sensor)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.ecowitt.value
|
17
homeassistant/components/ecowitt/strings.json
Normal file
17
homeassistant/components/ecowitt/strings.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"invalid_port": "Port is already used.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "The following steps must be performed to set up this integration.\n\nUse the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\nPick your station -> Menu Others -> DIY Upload Servers.\nHit next and select 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath have to match, you can copy with secure token /.\nSave configuration. The Ecowitt should then start attempting to send data to your server.",
|
||||
"data": {
|
||||
"port": "Listening port",
|
||||
"path": "Path with Security token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
homeassistant/components/ecowitt/translations/en.json
Normal file
17
homeassistant/components/ecowitt/translations/en.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"invalid_port": "Port is already used.",
|
||||
"unknown": "Unknown error."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "The following steps must be performed to set up this integration.\n\nUse the Ecowitt App (on your phone) or your Ecowitt WebUI over the station IP address.\nPick your station -> Menu Others -> DIY Upload Servers.\nHit next and select 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath have to match, you can copy with secure token /.\nSave configuration. The Ecowitt should then start attempting to send data to your server.",
|
||||
"data": {
|
||||
"port": "Listening port",
|
||||
"path": "Path with Security token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -90,6 +90,7 @@ FLOWS = {
|
|||
"eafm",
|
||||
"ecobee",
|
||||
"econet",
|
||||
"ecowitt",
|
||||
"efergy",
|
||||
"eight_sleep",
|
||||
"elgato",
|
||||
|
|
|
@ -146,6 +146,9 @@ aioeafm==0.1.2
|
|||
# homeassistant.components.rainforest_eagle
|
||||
aioeagle==1.1.0
|
||||
|
||||
# homeassistant.components.ecowitt
|
||||
aioecowitt==2022.08.3
|
||||
|
||||
# homeassistant.components.emonitor
|
||||
aioemonitor==1.0.5
|
||||
|
||||
|
|
|
@ -133,6 +133,9 @@ aioeafm==0.1.2
|
|||
# homeassistant.components.rainforest_eagle
|
||||
aioeagle==1.1.0
|
||||
|
||||
# homeassistant.components.ecowitt
|
||||
aioecowitt==2022.08.3
|
||||
|
||||
# homeassistant.components.emonitor
|
||||
aioemonitor==1.0.5
|
||||
|
||||
|
|
1
tests/components/ecowitt/__init__.py
Normal file
1
tests/components/ecowitt/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Ecowitt tests."""
|
110
tests/components/ecowitt/test_config_flow.py
Normal file
110
tests/components/ecowitt/test_config_flow.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
"""Test the Ecowitt Weather Station config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.ecowitt.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_create_entry(hass: HomeAssistant) -> None:
|
||||
"""Test we can create a config entry."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ecowitt.config_flow.EcoWittListener.start"
|
||||
), patch(
|
||||
"homeassistant.components.ecowitt.config_flow.EcoWittListener.stop"
|
||||
), patch(
|
||||
"homeassistant.components.ecowitt.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"port": 49911,
|
||||
"path": "/ecowitt-station",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Ecowitt on port 49911"
|
||||
assert result2["data"] == {
|
||||
"port": 49911,
|
||||
"path": "/ecowitt-station",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_invalid_port(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid port."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ecowitt.config_flow.EcoWittListener.start",
|
||||
side_effect=OSError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"port": 49911,
|
||||
"path": "/ecowitt-station",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "invalid_port"}
|
||||
|
||||
|
||||
async def test_already_configured_port(hass: HomeAssistant) -> None:
|
||||
"""Test already configured port."""
|
||||
MockConfigEntry(domain=DOMAIN, data={"port": 49911}).add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ecowitt.config_flow.EcoWittListener.start",
|
||||
side_effect=OSError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"port": 49911,
|
||||
"path": "/ecowitt-station",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.ABORT
|
||||
|
||||
|
||||
async def test_unknown_error(hass: HomeAssistant) -> None:
|
||||
"""Test we handle unknown error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ecowitt.config_flow.EcoWittListener.start",
|
||||
side_effect=Exception(),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"port": 49911,
|
||||
"path": "/ecowitt-station",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "unknown"}
|
Loading…
Add table
Add a link
Reference in a new issue