Add DROP integration (#104319)

* Add DROP integration

* Remove all but one platform for first PR

* Simplify initialization of hass.data[] structure

* Remove unnecessary mnemonic 'DROP_' prefix from DOMAIN constants

* Remove unnecessary whitespace

* Clarify configuration 'confirm' step description

* Remove unnecessary whitespace

* Use device class where applicable

* Remove unnecessary constructor and change its elements to class variables

* Change base entity inheritance to CoordinatorEntity

* Make sensor definitions more concise

* Rename HA domain from drop to drop_connect

* Remove underscores from class and function names

* Remove duplicate temperature sensor

* Change title capitalization

* Refactor using SensorEntityDescription

* Remove unnecessary intermediate dict layer

* Remove generated translations file

* Remove currently unused string values

* Use constants in sensor definitions

* Replace values with constants

* Move translation keys

* Remove unnecessary unique ID and config entry references

* Clean up DROPEntity initialization

* Clean up sensors

* Rename vars and functions according to style

* Remove redundant self references

* Clean up DROPSensor initializer

* Add missing state classes

* Simplify detection of configured devices

* Change entity identifiers to create device linkage

* Move device_info to coordinator

* Remove unnecessary properties

* Correct hub device IDs

* Remove redundant attribute

* Replace optional UID with assert

* Remove redundant attribute

* Correct coordinator initialization

* Fix mypy error

* Move API functionality to 3rd party library

* Abstract device to sensor map into a dict

* Unsubscribe MQTT on unload

* Move entity device information

* Make type checking for mypy conditional

* Bump dropmqttapi to 1.0.1

* Freeze dataclass to match parent class

* Fix race condition in MQTT unsubscribe setup

* Ensure unit tests begin with invalid MQTT state

* Change unit tests to reflect device firmware

* Move MQTT subscription out of the coordinator

* Tidy up initializer

* Move entirety of MQTT subscription out of the coordinator

* Make drop_api a class property

* Remove unnecessary type checks

* Simplify some unit test asserts

* Remove argument matching default

* Add entity category to battery and cartridge life sensors
This commit is contained in:
Patrick Frazer 2023-12-22 08:24:08 -05:00 committed by GitHub
parent 243ee2247b
commit fce1b6d248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1411 additions and 0 deletions

View File

@ -297,6 +297,8 @@ build.json @home-assistant/supervisor
/tests/components/dormakaba_dkey/ @emontnemery
/homeassistant/components/dremel_3d_printer/ @tkdrob
/tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
/homeassistant/components/dsmr/ @Robbie1221 @frenck
/tests/components/dsmr/ @Robbie1221 @frenck
/homeassistant/components/dsmr_reader/ @depl0y @glodenox

View File

@ -0,0 +1,66 @@
"""The drop_connect integration."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from homeassistant.components import mqtt
from homeassistant.components.mqtt import ReceiveMessage
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from .const import CONF_DATA_TOPIC, CONF_DEVICE_TYPE, DOMAIN
from .coordinator import DROPDeviceDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up DROP from a config entry."""
# Make sure MQTT integration is enabled and the client is available.
if not await mqtt.async_wait_for_mqtt_client(hass):
_LOGGER.error("MQTT integration is not available")
return False
if TYPE_CHECKING:
assert config_entry.unique_id is not None
drop_data_coordinator = DROPDeviceDataUpdateCoordinator(
hass, config_entry.unique_id
)
@callback
def mqtt_callback(msg: ReceiveMessage) -> None:
"""Pass MQTT payload to DROP API parser."""
if drop_data_coordinator.drop_api.parse_drop_message(
msg.topic, msg.payload, msg.qos, msg.retain
):
drop_data_coordinator.async_set_updated_data(None)
config_entry.async_on_unload(
await mqtt.async_subscribe(
hass, config_entry.data[CONF_DATA_TOPIC], mqtt_callback
)
)
_LOGGER.debug(
"Entry %s (%s) subscribed to %s",
config_entry.unique_id,
config_entry.data[CONF_DEVICE_TYPE],
config_entry.data[CONF_DATA_TOPIC],
)
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = drop_data_coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok

View File

@ -0,0 +1,98 @@
"""Config flow for drop_connect integration."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from dropmqttapi.discovery import DropDiscovery
from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from .const import (
CONF_COMMAND_TOPIC,
CONF_DATA_TOPIC,
CONF_DEVICE_DESC,
CONF_DEVICE_ID,
CONF_DEVICE_NAME,
CONF_DEVICE_OWNER_ID,
CONF_DEVICE_TYPE,
CONF_HUB_ID,
DISCOVERY_TOPIC,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle DROP config flow."""
VERSION = 1
_drop_discovery: DropDiscovery | None = None
async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult:
"""Handle a flow initialized by MQTT discovery."""
# Abort if the topic does not match our discovery topic or the payload is empty.
if (
discovery_info.subscribed_topic != DISCOVERY_TOPIC
or not discovery_info.payload
):
return self.async_abort(reason="invalid_discovery_info")
self._drop_discovery = DropDiscovery(DOMAIN)
if not (
await self._drop_discovery.parse_discovery(
discovery_info.topic, discovery_info.payload
)
):
return self.async_abort(reason="invalid_discovery_info")
existing_entry = await self.async_set_unique_id(
f"{self._drop_discovery.hub_id}_{self._drop_discovery.device_id}"
)
if existing_entry is not None:
# Note: returning "invalid_discovery_info" here instead of "already_configured"
# allows discovery of additional device types.
return self.async_abort(reason="invalid_discovery_info")
self.context.update({"title_placeholders": {"name": self._drop_discovery.name}})
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm the setup."""
if TYPE_CHECKING:
assert self._drop_discovery is not None
if user_input is not None:
device_data = {
CONF_COMMAND_TOPIC: self._drop_discovery.command_topic,
CONF_DATA_TOPIC: self._drop_discovery.data_topic,
CONF_DEVICE_DESC: self._drop_discovery.device_desc,
CONF_DEVICE_ID: self._drop_discovery.device_id,
CONF_DEVICE_NAME: self._drop_discovery.name,
CONF_DEVICE_TYPE: self._drop_discovery.device_type,
CONF_HUB_ID: self._drop_discovery.hub_id,
CONF_DEVICE_OWNER_ID: self._drop_discovery.owner_id,
}
return self.async_create_entry(
title=self._drop_discovery.name, data=device_data
)
return self.async_show_form(
step_id="confirm",
description_placeholders={
"device_name": self._drop_discovery.name,
"device_type": self._drop_discovery.device_desc,
},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
return self.async_abort(reason="not_supported")

View File

@ -0,0 +1,25 @@
"""Constants for the drop_connect integration."""
# Keys for values used in the config_entry data dictionary
CONF_COMMAND_TOPIC = "drop_command_topic"
CONF_DATA_TOPIC = "drop_data_topic"
CONF_DEVICE_DESC = "device_desc"
CONF_DEVICE_ID = "device_id"
CONF_DEVICE_TYPE = "device_type"
CONF_HUB_ID = "drop_hub_id"
CONF_DEVICE_NAME = "name"
CONF_DEVICE_OWNER_ID = "drop_device_owner_id"
# Values for DROP device types
DEV_FILTER = "filt"
DEV_HUB = "hub"
DEV_LEAK_DETECTOR = "leak"
DEV_PROTECTION_VALVE = "pv"
DEV_PUMP_CONTROLLER = "pc"
DEV_RO_FILTER = "ro"
DEV_SALT_SENSOR = "salt"
DEV_SOFTENER = "soft"
DISCOVERY_TOPIC = "drop_connect/discovery/#"
DOMAIN = "drop_connect"

View File

@ -0,0 +1,25 @@
"""DROP device data update coordinator object."""
from __future__ import annotations
import logging
from dropmqttapi.mqttapi import DropAPI
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class DROPDeviceDataUpdateCoordinator(DataUpdateCoordinator):
"""DROP device object."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, unique_id: str) -> None:
"""Initialize the device."""
super().__init__(hass, _LOGGER, name=f"{DOMAIN}-{unique_id}")
self.drop_api = DropAPI()

View File

@ -0,0 +1,53 @@
"""Base entity class for DROP entities."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
CONF_DEVICE_DESC,
CONF_DEVICE_NAME,
CONF_DEVICE_OWNER_ID,
CONF_DEVICE_TYPE,
CONF_HUB_ID,
DEV_HUB,
DOMAIN,
)
from .coordinator import DROPDeviceDataUpdateCoordinator
class DROPEntity(CoordinatorEntity[DROPDeviceDataUpdateCoordinator]):
"""Representation of a DROP device entity."""
_attr_has_entity_name = True
def __init__(
self, entity_type: str, coordinator: DROPDeviceDataUpdateCoordinator
) -> None:
"""Init DROP entity."""
super().__init__(coordinator)
if TYPE_CHECKING:
assert coordinator.config_entry.unique_id is not None
unique_id = coordinator.config_entry.unique_id
self._attr_unique_id = f"{unique_id}_{entity_type}"
entry_data = coordinator.config_entry.data
model: str = entry_data[CONF_DEVICE_DESC]
if entry_data[CONF_DEVICE_TYPE] == DEV_HUB:
model = f"Hub {entry_data[CONF_HUB_ID]}"
self._attr_device_info = DeviceInfo(
manufacturer="Chandler Systems, Inc.",
model=model,
name=entry_data[CONF_DEVICE_NAME],
identifiers={(DOMAIN, unique_id)},
)
if entry_data[CONF_DEVICE_TYPE] != DEV_HUB:
self._attr_device_info.update(
{
"via_device": (
DOMAIN,
entry_data[CONF_DEVICE_OWNER_ID],
)
}
)

View File

@ -0,0 +1,11 @@
{
"domain": "drop_connect",
"name": "DROP",
"codeowners": ["@ChandlerSystems", "@pfrazer"],
"config_flow": true,
"dependencies": ["mqtt"],
"documentation": "https://www.home-assistant.io/integrations/drop_connect",
"iot_class": "local_push",
"mqtt": ["drop_connect/discovery/#"],
"requirements": ["dropmqttapi==1.0.1"]
}

View File

@ -0,0 +1,285 @@
"""Support for DROP sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
EntityCategory,
UnitOfPressure,
UnitOfTemperature,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
CONF_DEVICE_TYPE,
DEV_FILTER,
DEV_HUB,
DEV_LEAK_DETECTOR,
DEV_PROTECTION_VALVE,
DEV_PUMP_CONTROLLER,
DEV_RO_FILTER,
DEV_SOFTENER,
DOMAIN,
)
from .coordinator import DROPDeviceDataUpdateCoordinator
from .entity import DROPEntity
_LOGGER = logging.getLogger(__name__)
FLOW_ICON = "mdi:shower-head"
GAUGE_ICON = "mdi:gauge"
TDS_ICON = "mdi:water-opacity"
# Sensor type constants
CURRENT_FLOW_RATE = "current_flow_rate"
PEAK_FLOW_RATE = "peak_flow_rate"
WATER_USED_TODAY = "water_used_today"
AVERAGE_WATER_USED = "average_water_used"
CAPACITY_REMAINING = "capacity_remaining"
CURRENT_SYSTEM_PRESSURE = "current_system_pressure"
HIGH_SYSTEM_PRESSURE = "high_system_pressure"
LOW_SYSTEM_PRESSURE = "low_system_pressure"
BATTERY = "battery"
TEMPERATURE = "temperature"
INLET_TDS = "inlet_tds"
OUTLET_TDS = "outlet_tds"
CARTRIDGE_1_LIFE = "cart1"
CARTRIDGE_2_LIFE = "cart2"
CARTRIDGE_3_LIFE = "cart3"
@dataclass(kw_only=True, frozen=True)
class DROPSensorEntityDescription(SensorEntityDescription):
"""Describes DROP sensor entity."""
value_fn: Callable[[DROPDeviceDataUpdateCoordinator], float | int | None]
SENSORS: list[DROPSensorEntityDescription] = [
DROPSensorEntityDescription(
key=CURRENT_FLOW_RATE,
translation_key=CURRENT_FLOW_RATE,
icon="mdi:shower-head",
native_unit_of_measurement="gpm",
suggested_display_precision=1,
value_fn=lambda device: device.drop_api.current_flow_rate(),
state_class=SensorStateClass.MEASUREMENT,
),
DROPSensorEntityDescription(
key=PEAK_FLOW_RATE,
translation_key=PEAK_FLOW_RATE,
icon="mdi:shower-head",
native_unit_of_measurement="gpm",
suggested_display_precision=1,
value_fn=lambda device: device.drop_api.peak_flow_rate(),
state_class=SensorStateClass.MEASUREMENT,
),
DROPSensorEntityDescription(
key=WATER_USED_TODAY,
translation_key=WATER_USED_TODAY,
device_class=SensorDeviceClass.WATER,
native_unit_of_measurement=UnitOfVolume.GALLONS,
suggested_display_precision=1,
value_fn=lambda device: device.drop_api.water_used_today(),
state_class=SensorStateClass.TOTAL,
),
DROPSensorEntityDescription(
key=AVERAGE_WATER_USED,
translation_key=AVERAGE_WATER_USED,
device_class=SensorDeviceClass.WATER,
native_unit_of_measurement=UnitOfVolume.GALLONS,
suggested_display_precision=0,
value_fn=lambda device: device.drop_api.average_water_used(),
state_class=SensorStateClass.TOTAL,
),
DROPSensorEntityDescription(
key=CAPACITY_REMAINING,
translation_key=CAPACITY_REMAINING,
device_class=SensorDeviceClass.WATER,
native_unit_of_measurement=UnitOfVolume.GALLONS,
suggested_display_precision=0,
value_fn=lambda device: device.drop_api.capacity_remaining(),
state_class=SensorStateClass.TOTAL,
),
DROPSensorEntityDescription(
key=CURRENT_SYSTEM_PRESSURE,
translation_key=CURRENT_SYSTEM_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
suggested_display_precision=1,
value_fn=lambda device: device.drop_api.current_system_pressure(),
state_class=SensorStateClass.MEASUREMENT,
),
DROPSensorEntityDescription(
key=HIGH_SYSTEM_PRESSURE,
translation_key=HIGH_SYSTEM_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
suggested_display_precision=0,
value_fn=lambda device: device.drop_api.high_system_pressure(),
state_class=SensorStateClass.MEASUREMENT,
),
DROPSensorEntityDescription(
key=LOW_SYSTEM_PRESSURE,
translation_key=LOW_SYSTEM_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
suggested_display_precision=0,
value_fn=lambda device: device.drop_api.low_system_pressure(),
state_class=SensorStateClass.MEASUREMENT,
),
DROPSensorEntityDescription(
key=BATTERY,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=0,
value_fn=lambda device: device.drop_api.battery(),
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
DROPSensorEntityDescription(
key=TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
suggested_display_precision=1,
value_fn=lambda device: device.drop_api.temperature(),
state_class=SensorStateClass.MEASUREMENT,
),
DROPSensorEntityDescription(
key=INLET_TDS,
translation_key=INLET_TDS,
icon=TDS_ICON,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
value_fn=lambda device: device.drop_api.inlet_tds(),
),
DROPSensorEntityDescription(
key=OUTLET_TDS,
translation_key=OUTLET_TDS,
icon=TDS_ICON,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
value_fn=lambda device: device.drop_api.outlet_tds(),
),
DROPSensorEntityDescription(
key=CARTRIDGE_1_LIFE,
translation_key=CARTRIDGE_1_LIFE,
icon=GAUGE_ICON,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=0,
value_fn=lambda device: device.drop_api.cart1(),
),
DROPSensorEntityDescription(
key=CARTRIDGE_2_LIFE,
translation_key=CARTRIDGE_2_LIFE,
icon=GAUGE_ICON,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=0,
value_fn=lambda device: device.drop_api.cart2(),
),
DROPSensorEntityDescription(
key=CARTRIDGE_3_LIFE,
translation_key=CARTRIDGE_3_LIFE,
icon=GAUGE_ICON,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=0,
value_fn=lambda device: device.drop_api.cart3(),
),
]
# Defines which sensors are used by each device type
DEVICE_SENSORS: dict[str, list[str]] = {
DEV_HUB: [
AVERAGE_WATER_USED,
BATTERY,
CURRENT_FLOW_RATE,
CURRENT_SYSTEM_PRESSURE,
HIGH_SYSTEM_PRESSURE,
LOW_SYSTEM_PRESSURE,
PEAK_FLOW_RATE,
WATER_USED_TODAY,
],
DEV_SOFTENER: [
BATTERY,
CAPACITY_REMAINING,
CURRENT_FLOW_RATE,
CURRENT_SYSTEM_PRESSURE,
],
DEV_FILTER: [BATTERY, CURRENT_FLOW_RATE, CURRENT_SYSTEM_PRESSURE],
DEV_LEAK_DETECTOR: [BATTERY, TEMPERATURE],
DEV_PROTECTION_VALVE: [
BATTERY,
CURRENT_FLOW_RATE,
CURRENT_SYSTEM_PRESSURE,
TEMPERATURE,
],
DEV_PUMP_CONTROLLER: [CURRENT_FLOW_RATE, CURRENT_SYSTEM_PRESSURE, TEMPERATURE],
DEV_RO_FILTER: [
CARTRIDGE_1_LIFE,
CARTRIDGE_2_LIFE,
CARTRIDGE_3_LIFE,
INLET_TDS,
OUTLET_TDS,
],
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the DROP sensors from config entry."""
_LOGGER.debug(
"Set up sensor for device type %s with entry_id is %s",
config_entry.data[CONF_DEVICE_TYPE],
config_entry.entry_id,
)
if config_entry.data[CONF_DEVICE_TYPE] in DEVICE_SENSORS:
async_add_entities(
DROPSensor(hass.data[DOMAIN][config_entry.entry_id], sensor)
for sensor in SENSORS
if sensor.key in DEVICE_SENSORS[config_entry.data[CONF_DEVICE_TYPE]]
)
class DROPSensor(DROPEntity, SensorEntity):
"""Representation of a DROP sensor."""
entity_description: DROPSensorEntityDescription
def __init__(
self,
coordinator: DROPDeviceDataUpdateCoordinator,
entity_description: DROPSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(entity_description.key, coordinator)
self.entity_description = entity_description
@property
def native_value(self) -> float | int | None:
"""Return the value reported by the sensor."""
return self.entity_description.value_fn(self.coordinator)

View File

@ -0,0 +1,30 @@
{
"config": {
"abort": {
"not_supported": "Configuration for DROP is through MQTT discovery. Use the DROP Connect app to connect your DROP Hub to your MQTT broker."
},
"step": {
"confirm": {
"title": "Confirm association",
"description": "Do you want to configure the DROP {device_type} named {device_name}?'"
}
}
},
"entity": {
"sensor": {
"current_flow_rate": { "name": "Water flow rate" },
"peak_flow_rate": { "name": "Peak water flow rate today" },
"water_used_today": { "name": "Total water used today" },
"average_water_used": { "name": "Average daily water usage" },
"capacity_remaining": { "name": "Capacity remaining" },
"current_system_pressure": { "name": "Current water pressure" },
"high_system_pressure": { "name": "High water pressure today" },
"low_system_pressure": { "name": "Low water pressure today" },
"inlet_tds": { "name": "Inlet TDS" },
"outlet_tds": { "name": "Outlet TDS" },
"cart1": { "name": "Cartridge 1 life remaining" },
"cart2": { "name": "Cartridge 2 life remaining" },
"cart3": { "name": "Cartridge 3 life remaining" }
}
}
}

View File

@ -111,6 +111,7 @@ FLOWS = {
"doorbird",
"dormakaba_dkey",
"dremel_3d_printer",
"drop_connect",
"dsmr",
"dsmr_reader",
"dunehd",

View File

@ -1253,6 +1253,12 @@
"config_flow": true,
"iot_class": "local_polling"
},
"drop_connect": {
"name": "DROP",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
"dsmr": {
"name": "DSMR Slimme Meter",
"integration_type": "hub",

View File

@ -4,6 +4,9 @@ To update, run python3 -m script.hassfest
"""
MQTT = {
"drop_connect": [
"drop_connect/discovery/#",
],
"dsmr_reader": [
"dsmr/#",
],

View File

@ -712,6 +712,9 @@ dovado==0.4.1
# homeassistant.components.dremel_3d_printer
dremel3dpy==2.1.1
# homeassistant.components.drop_connect
dropmqttapi==1.0.1
# homeassistant.components.dsmr
dsmr-parser==1.3.1

View File

@ -581,6 +581,9 @@ discovery30303==0.2.1
# homeassistant.components.dremel_3d_printer
dremel3dpy==2.1.1
# homeassistant.components.drop_connect
dropmqttapi==1.0.1
# homeassistant.components.dsmr
dsmr-parser==1.3.1

View File

@ -0,0 +1 @@
"""Tests for the DROP integration."""

View File

@ -0,0 +1,51 @@
"""Define common test values."""
TEST_DATA_HUB_TOPIC = "drop_connect/DROP-1_C0FFEE/255"
TEST_DATA_HUB = (
'{"curFlow":5.77,"peakFlow":13.8,"usedToday":232.77,"avgUsed":76,"psi":62.2,"psiLow":61,"psiHigh":62,'
'"water":1,"bypass":0,"pMode":"HOME","battery":50,"notif":1,"leak":0}'
)
TEST_DATA_HUB_RESET = (
'{"curFlow":0,"peakFlow":0,"usedToday":0,"avgUsed":0,"psi":0,"psiLow":0,"psiHigh":0,'
'"water":0,"bypass":0,"pMode":"AWAY","battery":0,"notif":0,"leak":0}'
)
TEST_DATA_SALT_TOPIC = "drop_connect/DROP-1_C0FFEE/8"
TEST_DATA_SALT = '{"salt":1}'
TEST_DATA_SALT_RESET = '{"salt":0}'
TEST_DATA_LEAK_TOPIC = "drop_connect/DROP-1_C0FFEE/20"
TEST_DATA_LEAK = '{"battery":100,"leak":1,"temp":68.2}'
TEST_DATA_LEAK_RESET = '{"battery":0,"leak":0,"temp":0}'
TEST_DATA_SOFTENER_TOPIC = "drop_connect/DROP-1_C0FFEE/0"
TEST_DATA_SOFTENER = (
'{"curFlow":5.0,"bypass":0,"battery":20,"capacity":1000,"resInUse":1,"psi":50.5}'
)
TEST_DATA_SOFTENER_RESET = (
'{"curFlow":0,"bypass":0,"battery":0,"capacity":0,"resInUse":0,"psi":null}'
)
TEST_DATA_FILTER_TOPIC = "drop_connect/DROP-1_C0FFEE/4"
TEST_DATA_FILTER = '{"curFlow":19.84,"bypass":0,"battery":12,"psi":38.2}'
TEST_DATA_FILTER_RESET = '{"curFlow":0,"bypass":0,"battery":0,"psi":null}'
TEST_DATA_PROTECTION_VALVE_TOPIC = "drop_connect/DROP-1_C0FFEE/78"
TEST_DATA_PROTECTION_VALVE = (
'{"curFlow":7.1,"psi":61.3,"water":1,"battery":0,"leak":1,"temp":70.5}'
)
TEST_DATA_PROTECTION_VALVE_RESET = (
'{"curFlow":0,"psi":0,"water":0,"battery":0,"leak":0,"temp":0}'
)
TEST_DATA_PUMP_CONTROLLER_TOPIC = "drop_connect/DROP-1_C0FFEE/83"
TEST_DATA_PUMP_CONTROLLER = '{"curFlow":2.2,"psi":62.2,"pump":1,"leak":1,"temp":68.8}'
TEST_DATA_PUMP_CONTROLLER_RESET = '{"curFlow":0,"psi":0,"pump":0,"leak":0,"temp":0}'
TEST_DATA_RO_FILTER_TOPIC = "drop_connect/DROP-1_C0FFEE/95"
TEST_DATA_RO_FILTER = (
'{"leak":1,"tdsIn":164,"tdsOut":9,"cart1":59,"cart2":80,"cart3":59}'
)
TEST_DATA_RO_FILTER_RESET = (
'{"leak":0,"tdsIn":0,"tdsOut":0,"cart1":0,"cart2":0,"cart3":0}'
)

View File

@ -0,0 +1,177 @@
"""Define fixtures available for all tests."""
import pytest
from homeassistant.components.drop_connect.const import (
CONF_COMMAND_TOPIC,
CONF_DATA_TOPIC,
CONF_DEVICE_DESC,
CONF_DEVICE_ID,
CONF_DEVICE_NAME,
CONF_DEVICE_OWNER_ID,
CONF_DEVICE_TYPE,
CONF_HUB_ID,
DOMAIN,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.fixture
def config_entry_hub(hass: HomeAssistant):
"""Config entry version 1 fixture."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="DROP-1_C0FFEE_255",
data={
CONF_COMMAND_TOPIC: "drop_connect/DROP-1_C0FFEE/255/cmd",
CONF_DATA_TOPIC: "drop_connect/DROP-1_C0FFEE/255/#",
CONF_DEVICE_DESC: "Hub",
CONF_DEVICE_ID: 255,
CONF_DEVICE_NAME: "Hub DROP-1_C0FFEE",
CONF_DEVICE_TYPE: "hub",
CONF_HUB_ID: "DROP-1_C0FFEE",
CONF_DEVICE_OWNER_ID: "DROP-1_C0FFEE_255",
},
version=1,
)
@pytest.fixture
def config_entry_salt(hass: HomeAssistant):
"""Config entry version 1 fixture."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="DROP-1_C0FFEE_8",
data={
CONF_COMMAND_TOPIC: "drop_connect/DROP-1_C0FFEE/8/cmd",
CONF_DATA_TOPIC: "drop_connect/DROP-1_C0FFEE/8/#",
CONF_DEVICE_DESC: "Salt Sensor",
CONF_DEVICE_ID: 8,
CONF_DEVICE_NAME: "Salt Sensor",
CONF_DEVICE_TYPE: "salt",
CONF_HUB_ID: "DROP-1_C0FFEE",
CONF_DEVICE_OWNER_ID: "DROP-1_C0FFEE_255",
},
version=1,
)
@pytest.fixture
def config_entry_leak(hass: HomeAssistant):
"""Config entry version 1 fixture."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="DROP-1_C0FFEE_20",
data={
CONF_COMMAND_TOPIC: "drop_connect/DROP-1_C0FFEE/20/cmd",
CONF_DATA_TOPIC: "drop_connect/DROP-1_C0FFEE/20/#",
CONF_DEVICE_DESC: "Leak Detector",
CONF_DEVICE_ID: 20,
CONF_DEVICE_NAME: "Leak Detector",
CONF_DEVICE_TYPE: "leak",
CONF_HUB_ID: "DROP-1_C0FFEE",
CONF_DEVICE_OWNER_ID: "DROP-1_C0FFEE_255",
},
version=1,
)
@pytest.fixture
def config_entry_softener(hass: HomeAssistant):
"""Config entry version 1 fixture."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="DROP-1_C0FFEE_0",
data={
CONF_COMMAND_TOPIC: "drop_connect/DROP-1_C0FFEE/0/cmd",
CONF_DATA_TOPIC: "drop_connect/DROP-1_C0FFEE/0/#",
CONF_DEVICE_DESC: "Softener",
CONF_DEVICE_ID: 0,
CONF_DEVICE_NAME: "Softener",
CONF_DEVICE_TYPE: "soft",
CONF_HUB_ID: "DROP-1_C0FFEE",
CONF_DEVICE_OWNER_ID: "DROP-1_C0FFEE_255",
},
version=1,
)
@pytest.fixture
def config_entry_filter(hass: HomeAssistant):
"""Config entry version 1 fixture."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="DROP-1_C0FFEE_4",
data={
CONF_COMMAND_TOPIC: "drop_connect/DROP-1_C0FFEE/4/cmd",
CONF_DATA_TOPIC: "drop_connect/DROP-1_C0FFEE/4/#",
CONF_DEVICE_DESC: "Filter",
CONF_DEVICE_ID: 4,
CONF_DEVICE_NAME: "Filter",
CONF_DEVICE_TYPE: "filt",
CONF_HUB_ID: "DROP-1_C0FFEE",
CONF_DEVICE_OWNER_ID: "DROP-1_C0FFEE_255",
},
version=1,
)
@pytest.fixture
def config_entry_protection_valve(hass: HomeAssistant):
"""Config entry version 1 fixture."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="DROP-1_C0FFEE_78",
data={
CONF_COMMAND_TOPIC: "drop_connect/DROP-1_C0FFEE/78/cmd",
CONF_DATA_TOPIC: "drop_connect/DROP-1_C0FFEE/78/#",
CONF_DEVICE_DESC: "Protection Valve",
CONF_DEVICE_ID: 78,
CONF_DEVICE_NAME: "Protection Valve",
CONF_DEVICE_TYPE: "pv",
CONF_HUB_ID: "DROP-1_C0FFEE",
CONF_DEVICE_OWNER_ID: "DROP-1_C0FFEE_255",
},
version=1,
)
@pytest.fixture
def config_entry_pump_controller(hass: HomeAssistant):
"""Config entry version 1 fixture."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="DROP-1_C0FFEE_83",
data={
CONF_COMMAND_TOPIC: "drop_connect/DROP-1_C0FFEE/83/cmd",
CONF_DATA_TOPIC: "drop_connect/DROP-1_C0FFEE/83/#",
CONF_DEVICE_DESC: "Pump Controller",
CONF_DEVICE_ID: 83,
CONF_DEVICE_NAME: "Pump Controller",
CONF_DEVICE_TYPE: "pc",
CONF_HUB_ID: "DROP-1_C0FFEE",
CONF_DEVICE_OWNER_ID: "DROP-1_C0FFEE_255",
},
version=1,
)
@pytest.fixture
def config_entry_ro_filter(hass: HomeAssistant):
"""Config entry version 1 fixture."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="DROP-1_C0FFEE_255",
data={
CONF_COMMAND_TOPIC: "drop_connect/DROP-1_C0FFEE/95/cmd",
CONF_DATA_TOPIC: "drop_connect/DROP-1_C0FFEE/95/#",
CONF_DEVICE_DESC: "RO Filter",
CONF_DEVICE_ID: 95,
CONF_DEVICE_NAME: "RO Filter",
CONF_DEVICE_TYPE: "ro",
CONF_HUB_ID: "DROP-1_C0FFEE",
CONF_DEVICE_OWNER_ID: "DROP-1_C0FFEE_255",
},
version=1,
)

View File

@ -0,0 +1,178 @@
"""Test config flow."""
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from tests.typing import MqttMockHAClient
async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None:
"""Test we can finish a config flow through MQTT with custom prefix."""
discovery_info = MqttServiceInfo(
topic="drop_connect/discovery/DROP-1_C0FFEE/255",
payload='{"devDesc":"Hub","devType":"hub","name":"Hub DROP-1_C0FFEE"}',
qos=0,
retain=False,
subscribed_topic="drop_connect/discovery/#",
timestamp=None,
)
result = await hass.config_entries.flow.async_init(
"drop_connect",
context={"source": config_entries.SOURCE_MQTT},
data=discovery_info,
)
assert result is not None
assert result["type"] == "form"
assert result["step_id"] == "confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
await hass.async_block_till_done()
assert result is not None
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
"drop_command_topic": "drop_connect/DROP-1_C0FFEE/cmd/255",
"drop_data_topic": "drop_connect/DROP-1_C0FFEE/data/255/#",
"device_desc": "Hub",
"device_id": "255",
"name": "Hub DROP-1_C0FFEE",
"device_type": "hub",
"drop_hub_id": "DROP-1_C0FFEE",
"drop_device_owner_id": "DROP-1_C0FFEE_255",
}
async def test_duplicate(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None:
"""Test we can finish a config flow through MQTT with custom prefix."""
discovery_info = MqttServiceInfo(
topic="drop_connect/discovery/DROP-1_C0FFEE/255",
payload='{"devDesc":"Hub","devType":"hub","name":"Hub DROP-1_C0FFEE"}',
qos=0,
retain=False,
subscribed_topic="drop_connect/discovery/#",
timestamp=None,
)
result = await hass.config_entries.flow.async_init(
"drop_connect",
context={"source": config_entries.SOURCE_MQTT},
data=discovery_info,
)
assert result is not None
assert result["type"] == "form"
assert result["step_id"] == "confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
await hass.async_block_till_done()
assert result is not None
assert result["type"] == FlowResultType.CREATE_ENTRY
# Attempting configuration of the same object should abort
result = await hass.config_entries.flow.async_init(
"drop_connect",
context={"source": config_entries.SOURCE_MQTT},
data=discovery_info,
)
assert result is not None
assert result["type"] == "abort"
assert result["reason"] == "invalid_discovery_info"
async def test_mqtt_setup_incomplete_payload(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test we can finish a config flow through MQTT with custom prefix."""
discovery_info = MqttServiceInfo(
topic="drop_connect/discovery/DROP-1_C0FFEE/255",
payload='{"devDesc":"Hub"}',
qos=0,
retain=False,
subscribed_topic="drop_connect/discovery/#",
timestamp=None,
)
result = await hass.config_entries.flow.async_init(
"drop_connect",
context={"source": config_entries.SOURCE_MQTT},
data=discovery_info,
)
assert result is not None
assert result["type"] == "abort"
assert result["reason"] == "invalid_discovery_info"
async def test_mqtt_setup_bad_json(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test we can finish a config flow through MQTT with custom prefix."""
discovery_info = MqttServiceInfo(
topic="drop_connect/discovery/DROP-1_C0FFEE/255",
payload="{BAD JSON}",
qos=0,
retain=False,
subscribed_topic="drop_connect/discovery/#",
timestamp=None,
)
result = await hass.config_entries.flow.async_init(
"drop_connect",
context={"source": config_entries.SOURCE_MQTT},
data=discovery_info,
)
assert result is not None
assert result["type"] == "abort"
assert result["reason"] == "invalid_discovery_info"
async def test_mqtt_setup_bad_topic(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test we can finish a config flow through MQTT with custom prefix."""
discovery_info = MqttServiceInfo(
topic="drop_connect/discovery/FOO",
payload=('{"devDesc":"Hub","devType":"hub","name":"Hub DROP-1_C0FFEE"}'),
qos=0,
retain=False,
subscribed_topic="drop_connect/discovery/#",
timestamp=None,
)
result = await hass.config_entries.flow.async_init(
"drop_connect",
context={"source": config_entries.SOURCE_MQTT},
data=discovery_info,
)
assert result is not None
assert result["type"] == "abort"
assert result["reason"] == "invalid_discovery_info"
async def test_mqtt_setup_no_payload(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test we can finish a config flow through MQTT with custom prefix."""
discovery_info = MqttServiceInfo(
topic="drop_connect/discovery/DROP-1_C0FFEE/255",
payload="",
qos=0,
retain=False,
subscribed_topic="drop_connect/discovery/#",
timestamp=None,
)
result = await hass.config_entries.flow.async_init(
"drop_connect",
context={"source": config_entries.SOURCE_MQTT},
data=discovery_info,
)
assert result is not None
assert result["type"] == "abort"
assert result["reason"] == "invalid_discovery_info"
async def test_user_setup(hass: HomeAssistant) -> None:
"""Test user setup."""
result = await hass.config_entries.flow.async_init(
"drop_connect", context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "abort"
assert result["reason"] == "not_supported"

View File

@ -0,0 +1,74 @@
"""Test DROP coordinator."""
from homeassistant.components.drop_connect.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .common import TEST_DATA_HUB, TEST_DATA_HUB_RESET, TEST_DATA_HUB_TOPIC
from tests.common import async_fire_mqtt_message
from tests.typing import MqttMockHAClient
async def test_bad_json(
hass: HomeAssistant, config_entry_hub, mqtt_mock: MqttMockHAClient
) -> None:
"""Test bad JSON."""
config_entry_hub.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
current_flow_sensor_name = "sensor.hub_drop_1_c0ffee_water_flow_rate"
hass.states.async_set(current_flow_sensor_name, STATE_UNKNOWN)
async_fire_mqtt_message(hass, TEST_DATA_HUB_TOPIC, "{BAD JSON}")
await hass.async_block_till_done()
current_flow_sensor = hass.states.get(current_flow_sensor_name)
assert current_flow_sensor
assert current_flow_sensor.state == STATE_UNKNOWN
async def test_unload(
hass: HomeAssistant, config_entry_hub, mqtt_mock: MqttMockHAClient
) -> None:
"""Test entity unload."""
# Load the hub device
config_entry_hub.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
current_flow_sensor_name = "sensor.hub_drop_1_c0ffee_water_flow_rate"
hass.states.async_set(current_flow_sensor_name, STATE_UNKNOWN)
async_fire_mqtt_message(hass, TEST_DATA_HUB_TOPIC, TEST_DATA_HUB_RESET)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, TEST_DATA_HUB_TOPIC, TEST_DATA_HUB)
await hass.async_block_till_done()
current_flow_sensor = hass.states.get(current_flow_sensor_name)
assert current_flow_sensor
assert round(float(current_flow_sensor.state), 1) == 5.8
# Unload the device
await hass.config_entries.async_unload(config_entry_hub.entry_id)
await hass.async_block_till_done()
assert config_entry_hub.state is ConfigEntryState.NOT_LOADED
# Verify sensor is unavailable
current_flow_sensor = hass.states.get(current_flow_sensor_name)
assert current_flow_sensor
assert current_flow_sensor.state == STATE_UNAVAILABLE
async def test_no_mqtt(hass: HomeAssistant, config_entry_hub) -> None:
"""Test no MQTT."""
config_entry_hub.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
protect_mode_select_name = "select.hub_drop_1_c0ffee_protect_mode"
protect_mode_select = hass.states.get(protect_mode_select_name)
assert protect_mode_select is None

View File

@ -0,0 +1,319 @@
"""Test DROP sensor entities."""
from homeassistant.components.drop_connect.const import DOMAIN
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .common import (
TEST_DATA_FILTER,
TEST_DATA_FILTER_RESET,
TEST_DATA_FILTER_TOPIC,
TEST_DATA_HUB,
TEST_DATA_HUB_RESET,
TEST_DATA_HUB_TOPIC,
TEST_DATA_LEAK,
TEST_DATA_LEAK_RESET,
TEST_DATA_LEAK_TOPIC,
TEST_DATA_PROTECTION_VALVE,
TEST_DATA_PROTECTION_VALVE_RESET,
TEST_DATA_PROTECTION_VALVE_TOPIC,
TEST_DATA_PUMP_CONTROLLER,
TEST_DATA_PUMP_CONTROLLER_RESET,
TEST_DATA_PUMP_CONTROLLER_TOPIC,
TEST_DATA_RO_FILTER,
TEST_DATA_RO_FILTER_RESET,
TEST_DATA_RO_FILTER_TOPIC,
TEST_DATA_SOFTENER,
TEST_DATA_SOFTENER_RESET,
TEST_DATA_SOFTENER_TOPIC,
)
from tests.common import async_fire_mqtt_message
from tests.typing import MqttMockHAClient
async def test_sensors_hub(
hass: HomeAssistant, config_entry_hub, mqtt_mock: MqttMockHAClient
) -> None:
"""Test DROP sensors for hubs."""
config_entry_hub.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
current_flow_sensor_name = "sensor.hub_drop_1_c0ffee_water_flow_rate"
hass.states.async_set(current_flow_sensor_name, STATE_UNKNOWN)
peak_flow_sensor_name = "sensor.hub_drop_1_c0ffee_peak_water_flow_rate_today"
hass.states.async_set(peak_flow_sensor_name, STATE_UNKNOWN)
used_today_sensor_name = "sensor.hub_drop_1_c0ffee_total_water_used_today"
hass.states.async_set(used_today_sensor_name, STATE_UNKNOWN)
average_usage_sensor_name = "sensor.hub_drop_1_c0ffee_average_daily_water_usage"
hass.states.async_set(average_usage_sensor_name, STATE_UNKNOWN)
psi_sensor_name = "sensor.hub_drop_1_c0ffee_current_water_pressure"
hass.states.async_set(psi_sensor_name, STATE_UNKNOWN)
psi_high_sensor_name = "sensor.hub_drop_1_c0ffee_high_water_pressure_today"
hass.states.async_set(psi_high_sensor_name, STATE_UNKNOWN)
psi_low_sensor_name = "sensor.hub_drop_1_c0ffee_low_water_pressure_today"
hass.states.async_set(psi_low_sensor_name, STATE_UNKNOWN)
battery_sensor_name = "sensor.hub_drop_1_c0ffee_battery"
hass.states.async_set(battery_sensor_name, STATE_UNKNOWN)
async_fire_mqtt_message(hass, TEST_DATA_HUB_TOPIC, TEST_DATA_HUB_RESET)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, TEST_DATA_HUB_TOPIC, TEST_DATA_HUB)
await hass.async_block_till_done()
current_flow_sensor = hass.states.get(current_flow_sensor_name)
assert current_flow_sensor
assert round(float(current_flow_sensor.state), 1) == 5.8
peak_flow_sensor = hass.states.get(peak_flow_sensor_name)
assert peak_flow_sensor
assert round(float(peak_flow_sensor.state), 1) == 13.8
used_today_sensor = hass.states.get(used_today_sensor_name)
assert used_today_sensor
assert round(float(used_today_sensor.state), 1) == 881.1 # liters
average_usage_sensor = hass.states.get(average_usage_sensor_name)
assert average_usage_sensor
assert round(float(average_usage_sensor.state), 1) == 287.7 # liters
psi_sensor = hass.states.get(psi_sensor_name)
assert psi_sensor
assert round(float(psi_sensor.state), 1) == 428.9 # centibars
psi_high_sensor = hass.states.get(psi_high_sensor_name)
assert psi_high_sensor
assert round(float(psi_high_sensor.state), 1) == 427.5 # centibars
psi_low_sensor = hass.states.get(psi_low_sensor_name)
assert psi_low_sensor
assert round(float(psi_low_sensor.state), 1) == 420.6 # centibars
battery_sensor = hass.states.get(battery_sensor_name)
assert battery_sensor
assert int(battery_sensor.state) == 50
async def test_sensors_leak(
hass: HomeAssistant, config_entry_leak, mqtt_mock: MqttMockHAClient
) -> None:
"""Test DROP sensors for leak detectors."""
config_entry_leak.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
battery_sensor_name = "sensor.leak_detector_battery"
hass.states.async_set(battery_sensor_name, STATE_UNKNOWN)
temp_sensor_name = "sensor.leak_detector_temperature"
hass.states.async_set(temp_sensor_name, STATE_UNKNOWN)
async_fire_mqtt_message(hass, TEST_DATA_LEAK_TOPIC, TEST_DATA_LEAK_RESET)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, TEST_DATA_LEAK_TOPIC, TEST_DATA_LEAK)
await hass.async_block_till_done()
battery_sensor = hass.states.get(battery_sensor_name)
assert battery_sensor
assert int(battery_sensor.state) == 100
temp_sensor = hass.states.get(temp_sensor_name)
assert temp_sensor
assert round(float(temp_sensor.state), 1) == 20.1 # C
async def test_sensors_softener(
hass: HomeAssistant, config_entry_softener, mqtt_mock: MqttMockHAClient
) -> None:
"""Test DROP sensors for softeners."""
config_entry_softener.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
battery_sensor_name = "sensor.softener_battery"
hass.states.async_set(battery_sensor_name, STATE_UNKNOWN)
current_flow_sensor_name = "sensor.softener_water_flow_rate"
hass.states.async_set(current_flow_sensor_name, STATE_UNKNOWN)
psi_sensor_name = "sensor.softener_current_water_pressure"
hass.states.async_set(psi_sensor_name, STATE_UNKNOWN)
capacity_sensor_name = "sensor.softener_capacity_remaining"
hass.states.async_set(capacity_sensor_name, STATE_UNKNOWN)
async_fire_mqtt_message(hass, TEST_DATA_SOFTENER_TOPIC, TEST_DATA_SOFTENER_RESET)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, TEST_DATA_SOFTENER_TOPIC, TEST_DATA_SOFTENER)
await hass.async_block_till_done()
battery_sensor = hass.states.get(battery_sensor_name)
assert battery_sensor
assert int(battery_sensor.state) == 20
current_flow_sensor = hass.states.get(current_flow_sensor_name)
assert current_flow_sensor
assert round(float(current_flow_sensor.state), 1) == 5.0
psi_sensor = hass.states.get(psi_sensor_name)
assert psi_sensor
assert round(float(psi_sensor.state), 1) == 348.2 # centibars
capacity_sensor = hass.states.get(capacity_sensor_name)
assert capacity_sensor
assert round(float(capacity_sensor.state), 1) == 3785.4 # liters
async def test_sensors_filter(
hass: HomeAssistant, config_entry_filter, mqtt_mock: MqttMockHAClient
) -> None:
"""Test DROP sensors for filters."""
config_entry_filter.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
battery_sensor_name = "sensor.filter_battery"
hass.states.async_set(battery_sensor_name, STATE_UNKNOWN)
current_flow_sensor_name = "sensor.filter_water_flow_rate"
hass.states.async_set(current_flow_sensor_name, STATE_UNKNOWN)
psi_sensor_name = "sensor.filter_current_water_pressure"
hass.states.async_set(psi_sensor_name, STATE_UNKNOWN)
async_fire_mqtt_message(hass, TEST_DATA_FILTER_TOPIC, TEST_DATA_FILTER_RESET)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, TEST_DATA_FILTER_TOPIC, TEST_DATA_FILTER)
await hass.async_block_till_done()
battery_sensor = hass.states.get(battery_sensor_name)
assert battery_sensor
assert round(float(battery_sensor.state), 1) == 12.0
current_flow_sensor = hass.states.get(current_flow_sensor_name)
assert current_flow_sensor
assert round(float(current_flow_sensor.state), 1) == 19.8
psi_sensor = hass.states.get(psi_sensor_name)
assert psi_sensor
assert round(float(psi_sensor.state), 1) == 263.4 # centibars
async def test_sensors_protection_valve(
hass: HomeAssistant, config_entry_protection_valve, mqtt_mock: MqttMockHAClient
) -> None:
"""Test DROP sensors for protection valves."""
config_entry_protection_valve.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
battery_sensor_name = "sensor.protection_valve_battery"
hass.states.async_set(battery_sensor_name, STATE_UNKNOWN)
current_flow_sensor_name = "sensor.protection_valve_water_flow_rate"
hass.states.async_set(current_flow_sensor_name, STATE_UNKNOWN)
psi_sensor_name = "sensor.protection_valve_current_water_pressure"
hass.states.async_set(psi_sensor_name, STATE_UNKNOWN)
temp_sensor_name = "sensor.protection_valve_temperature"
hass.states.async_set(temp_sensor_name, STATE_UNKNOWN)
async_fire_mqtt_message(
hass, TEST_DATA_PROTECTION_VALVE_TOPIC, TEST_DATA_PROTECTION_VALVE_RESET
)
await hass.async_block_till_done()
async_fire_mqtt_message(
hass, TEST_DATA_PROTECTION_VALVE_TOPIC, TEST_DATA_PROTECTION_VALVE
)
await hass.async_block_till_done()
battery_sensor = hass.states.get(battery_sensor_name)
assert battery_sensor
assert int(battery_sensor.state) == 0
current_flow_sensor = hass.states.get(current_flow_sensor_name)
assert current_flow_sensor
assert round(float(current_flow_sensor.state), 1) == 7.1
psi_sensor = hass.states.get(psi_sensor_name)
assert psi_sensor
assert round(float(psi_sensor.state), 1) == 422.6 # centibars
temp_sensor = hass.states.get(temp_sensor_name)
assert temp_sensor
assert round(float(temp_sensor.state), 1) == 21.4 # C
async def test_sensors_pump_controller(
hass: HomeAssistant, config_entry_pump_controller, mqtt_mock: MqttMockHAClient
) -> None:
"""Test DROP sensors for pump controllers."""
config_entry_pump_controller.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
current_flow_sensor_name = "sensor.pump_controller_water_flow_rate"
hass.states.async_set(current_flow_sensor_name, STATE_UNKNOWN)
psi_sensor_name = "sensor.pump_controller_current_water_pressure"
hass.states.async_set(psi_sensor_name, STATE_UNKNOWN)
temp_sensor_name = "sensor.pump_controller_temperature"
hass.states.async_set(temp_sensor_name, STATE_UNKNOWN)
async_fire_mqtt_message(
hass, TEST_DATA_PUMP_CONTROLLER_TOPIC, TEST_DATA_PUMP_CONTROLLER_RESET
)
await hass.async_block_till_done()
async_fire_mqtt_message(
hass, TEST_DATA_PUMP_CONTROLLER_TOPIC, TEST_DATA_PUMP_CONTROLLER
)
await hass.async_block_till_done()
current_flow_sensor = hass.states.get(current_flow_sensor_name)
assert current_flow_sensor
assert round(float(current_flow_sensor.state), 1) == 2.2
psi_sensor = hass.states.get(psi_sensor_name)
assert psi_sensor
assert round(float(psi_sensor.state), 1) == 428.9 # centibars
temp_sensor = hass.states.get(temp_sensor_name)
assert temp_sensor
assert round(float(temp_sensor.state), 1) == 20.4 # C
async def test_sensors_ro_filter(
hass: HomeAssistant, config_entry_ro_filter, mqtt_mock: MqttMockHAClient
) -> None:
"""Test DROP sensors for RO filters."""
config_entry_ro_filter.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
tds_in_sensor_name = "sensor.ro_filter_inlet_tds"
hass.states.async_set(tds_in_sensor_name, STATE_UNKNOWN)
tds_out_sensor_name = "sensor.ro_filter_outlet_tds"
hass.states.async_set(tds_out_sensor_name, STATE_UNKNOWN)
cart1_sensor_name = "sensor.ro_filter_cartridge_1_life_remaining"
hass.states.async_set(cart1_sensor_name, STATE_UNKNOWN)
cart2_sensor_name = "sensor.ro_filter_cartridge_2_life_remaining"
hass.states.async_set(cart2_sensor_name, STATE_UNKNOWN)
cart3_sensor_name = "sensor.ro_filter_cartridge_3_life_remaining"
hass.states.async_set(cart3_sensor_name, STATE_UNKNOWN)
async_fire_mqtt_message(hass, TEST_DATA_RO_FILTER_TOPIC, TEST_DATA_RO_FILTER_RESET)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, TEST_DATA_RO_FILTER_TOPIC, TEST_DATA_RO_FILTER)
await hass.async_block_till_done()
tds_in_sensor = hass.states.get(tds_in_sensor_name)
assert tds_in_sensor
assert int(tds_in_sensor.state) == 164
tds_out_sensor = hass.states.get(tds_out_sensor_name)
assert tds_out_sensor
assert int(tds_out_sensor.state) == 9
cart1_sensor = hass.states.get(cart1_sensor_name)
assert cart1_sensor
assert int(cart1_sensor.state) == 59
cart2_sensor = hass.states.get(cart2_sensor_name)
assert cart2_sensor
assert int(cart2_sensor.state) == 80
cart3_sensor = hass.states.get(cart3_sensor_name)
assert cart3_sensor
assert int(cart3_sensor.state) == 59