mirror of
https://github.com/home-assistant/core.git
synced 2026-05-05 12:24:48 +02:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 447901c223 | |||
| 9dcd3f6626 | |||
| d34bd8ad1e | |||
| 83e4e4f769 | |||
| 128dc07fa5 | |||
| d2dfdd81ad | |||
| 0442827b9e | |||
| 37c3062874 | |||
| bfacff5d78 | |||
| d54621e778 | |||
| 716c3f69ca | |||
| 630a1fb36c | |||
| 54eeebfd20 | |||
| a671a0ccac | |||
| 8cf0182f2f | |||
| f1400b03bb | |||
| 6dc00d3d87 | |||
| bf6133534d | |||
| b1758e1fcc | |||
| cc0aa32f3e | |||
| dc2494f0a0 | |||
| 4b2a1ec694 | |||
| b2187022c4 | |||
| c6f588fc08 | |||
| 462e3a3d0d | |||
| aa179a1ad9 | |||
| 1117158bd0 | |||
| 6ced0153df | |||
| 7f314e17de | |||
| 9ad29ae75c | |||
| 43a89dc452 | |||
| 268f0dd62f | |||
| d7768f13c1 | |||
| db8aa4658a | |||
| d19d487b21 | |||
| 2aeecba64c | |||
| b3367d8b3f | |||
| 7e6856ace8 | |||
| b5f0c2cef4 | |||
| 2ffc779f3d |
@@ -75,12 +75,6 @@ omit =
|
||||
homeassistant/components/asuswrt/router.py
|
||||
homeassistant/components/aten_pe/*
|
||||
homeassistant/components/atome/*
|
||||
homeassistant/components/automate/__init__.py
|
||||
homeassistant/components/automate/base.py
|
||||
homeassistant/components/automate/const.py
|
||||
homeassistant/components/automate/cover.py
|
||||
homeassistant/components/automate/helpers.py
|
||||
homeassistant/components/automate/hub.py
|
||||
homeassistant/components/aurora/__init__.py
|
||||
homeassistant/components/aurora/binary_sensor.py
|
||||
homeassistant/components/aurora/const.py
|
||||
|
||||
@@ -56,7 +56,6 @@ homeassistant/components/august/* @bdraco
|
||||
homeassistant/components/aurora/* @djtimca
|
||||
homeassistant/components/aurora_abb_powerone/* @davet2001
|
||||
homeassistant/components/auth/* @home-assistant/core
|
||||
homeassistant/components/automate/* @sillyfrog
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/avea/* @pattyland
|
||||
homeassistant/components/awair/* @ahayworth @danielsjf
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Apple TV",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||
"requirements": ["pyatv==0.8.1"],
|
||||
"requirements": ["pyatv==0.8.2"],
|
||||
"zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."],
|
||||
"after_dependencies": ["discovery"],
|
||||
"codeowners": ["@postlund"],
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"""The Automate Pulse Hub v2 integration."""
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .hub import PulseHub
|
||||
|
||||
PLATFORMS = ["cover"]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Automate Pulse Hub v2 from a config entry."""
|
||||
hub = PulseHub(hass, entry)
|
||||
|
||||
if not await hub.async_setup():
|
||||
return False
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = hub
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hub = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
if not await hub.async_reset():
|
||||
return False
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
@@ -1,93 +0,0 @@
|
||||
"""Base class for Automate Roller Blinds."""
|
||||
import logging
|
||||
|
||||
import aiopulse2
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import entity
|
||||
from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg
|
||||
|
||||
from .const import AUTOMATE_ENTITY_REMOVE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AutomateBase(entity.Entity):
|
||||
"""Base representation of an Automate roller."""
|
||||
|
||||
def __init__(self, roller: aiopulse2.Roller) -> None:
|
||||
"""Initialize the roller."""
|
||||
self.roller = roller
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if roller and hub is available."""
|
||||
return self.roller.online and self.roller.hub.connected
|
||||
|
||||
async def async_remove_and_unregister(self):
|
||||
"""Unregister from entity and device registry and call entity remove function."""
|
||||
_LOGGER.info("Removing %s %s", self.__class__.__name__, self.unique_id)
|
||||
|
||||
ent_registry = await get_ent_reg(self.hass)
|
||||
if self.entity_id in ent_registry.entities:
|
||||
ent_registry.async_remove(self.entity_id)
|
||||
|
||||
dev_registry = await get_dev_reg(self.hass)
|
||||
device = dev_registry.async_get_device(
|
||||
identifiers={(DOMAIN, self.unique_id)}, connections=set()
|
||||
)
|
||||
if device is not None:
|
||||
dev_registry.async_update_device(
|
||||
device.id, remove_config_entry_id=self.registry_entry.config_entry_id
|
||||
)
|
||||
|
||||
await self.async_remove()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Entity has been added to hass."""
|
||||
self.roller.callback_subscribe(self.notify_update)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
AUTOMATE_ENTITY_REMOVE.format(self.roller.id),
|
||||
self.async_remove_and_unregister,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Entity being removed from hass."""
|
||||
self.roller.callback_unsubscribe(self.notify_update)
|
||||
|
||||
@callback
|
||||
def notify_update(self, roller: aiopulse2.Roller):
|
||||
"""Write updated device state information."""
|
||||
_LOGGER.debug(
|
||||
"Device update notification received: %s (%r)", roller.id, roller.name
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Report that Automate entities do not need polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of this roller."""
|
||||
return self.roller.id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of roller."""
|
||||
return self.roller.name
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
attrs = {
|
||||
"identifiers": {(DOMAIN, self.roller.id)},
|
||||
}
|
||||
return attrs
|
||||
@@ -1,37 +0,0 @@
|
||||
"""Config flow for Automate Pulse Hub v2 integration."""
|
||||
import logging
|
||||
|
||||
import aiopulse2
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema({vol.Required("host"): str})
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Automate Pulse Hub v2."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step once we have info from the user."""
|
||||
if user_input is not None:
|
||||
try:
|
||||
hub = aiopulse2.Hub(user_input["host"])
|
||||
await hub.test()
|
||||
title = hub.name
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA,
|
||||
errors={"base": "cannot_connect"},
|
||||
)
|
||||
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Constants for the Automate Pulse Hub v2 integration."""
|
||||
|
||||
DOMAIN = "automate"
|
||||
|
||||
AUTOMATE_HUB_UPDATE = "automate_hub_update_{}"
|
||||
AUTOMATE_ENTITY_REMOVE = "automate_entity_remove_{}"
|
||||
@@ -1,147 +0,0 @@
|
||||
"""Support for Automate Roller Blinds."""
|
||||
import aiopulse2
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
DEVICE_CLASS_SHADE,
|
||||
SUPPORT_CLOSE,
|
||||
SUPPORT_CLOSE_TILT,
|
||||
SUPPORT_OPEN,
|
||||
SUPPORT_OPEN_TILT,
|
||||
SUPPORT_SET_POSITION,
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_STOP_TILT,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .base import AutomateBase
|
||||
from .const import AUTOMATE_HUB_UPDATE, DOMAIN
|
||||
from .helpers import async_add_automate_entities
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Automate Rollers from a config entry."""
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
current = set()
|
||||
|
||||
@callback
|
||||
def async_add_automate_covers():
|
||||
async_add_automate_entities(
|
||||
hass, AutomateCover, config_entry, current, async_add_entities
|
||||
)
|
||||
|
||||
hub.cleanup_callbacks.append(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
AUTOMATE_HUB_UPDATE.format(config_entry.entry_id),
|
||||
async_add_automate_covers,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AutomateCover(AutomateBase, CoverEntity):
|
||||
"""Representation of a Automate cover device."""
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of the roller blind.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
position = None
|
||||
if self.roller.closed_percent is not None:
|
||||
position = 100 - self.roller.closed_percent
|
||||
return position
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return the current tilt of the roller blind.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = 0
|
||||
if self.current_cover_position is not None:
|
||||
supported_features |= (
|
||||
SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION
|
||||
)
|
||||
if self.current_cover_tilt_position is not None:
|
||||
supported_features |= (
|
||||
SUPPORT_OPEN_TILT
|
||||
| SUPPORT_CLOSE_TILT
|
||||
| SUPPORT_STOP_TILT
|
||||
| SUPPORT_SET_TILT_POSITION
|
||||
)
|
||||
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
attrs = super().device_info
|
||||
attrs["manufacturer"] = "Automate"
|
||||
attrs["model"] = self.roller.devicetype
|
||||
attrs["sw_version"] = self.roller.version
|
||||
attrs["via_device"] = (DOMAIN, self.roller.hub.id)
|
||||
attrs["name"] = self.name
|
||||
return attrs
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Class of the cover, a shade."""
|
||||
return DEVICE_CLASS_SHADE
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Is cover opening/moving up."""
|
||||
return self.roller.action == aiopulse2.MovingAction.up
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Is cover closing/moving down."""
|
||||
return self.roller.action == aiopulse2.MovingAction.down
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self.roller.closed_percent == 100
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Close the roller."""
|
||||
await self.roller.move_down()
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Open the roller."""
|
||||
await self.roller.move_up()
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop the roller."""
|
||||
await self.roller.move_stop()
|
||||
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
await self.roller.move_to(100 - kwargs[ATTR_POSITION])
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs):
|
||||
"""Close the roller."""
|
||||
await self.roller.move_down()
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs):
|
||||
"""Open the roller."""
|
||||
await self.roller.move_up()
|
||||
|
||||
async def async_stop_cover_tilt(self, **kwargs):
|
||||
"""Stop the roller."""
|
||||
await self.roller.move_stop()
|
||||
|
||||
async def async_set_cover_tilt(self, **kwargs):
|
||||
"""Tilt the roller shutter to a specific position."""
|
||||
await self.roller.move_to(100 - kwargs[ATTR_POSITION])
|
||||
@@ -1,46 +0,0 @@
|
||||
"""Helper functions for Automate Pulse."""
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_automate_entities(
|
||||
hass, entity_class, config_entry, current, async_add_entities
|
||||
):
|
||||
"""Add any new entities."""
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
_LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host)
|
||||
|
||||
api = hub.api.rollers
|
||||
|
||||
new_items = []
|
||||
for unique_id, roller in api.items():
|
||||
if unique_id not in current:
|
||||
_LOGGER.debug("New %s %s", entity_class.__name__, unique_id)
|
||||
new_item = entity_class(roller)
|
||||
current.add(unique_id)
|
||||
new_items.append(new_item)
|
||||
|
||||
async_add_entities(new_items)
|
||||
|
||||
|
||||
async def update_devices(hass, config_entry, api):
|
||||
"""Tell hass that device info has been updated."""
|
||||
dev_registry = await get_dev_reg(hass)
|
||||
|
||||
for api_item in api.values():
|
||||
# Update Device name
|
||||
device = dev_registry.async_get_device(
|
||||
identifiers={(DOMAIN, api_item.id)}, connections=set()
|
||||
)
|
||||
if device is not None:
|
||||
dev_registry.async_update_device(
|
||||
device.id,
|
||||
name=api_item.name,
|
||||
)
|
||||
@@ -1,89 +0,0 @@
|
||||
"""Code to handle a Pulse Hub."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiopulse2
|
||||
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import AUTOMATE_ENTITY_REMOVE, AUTOMATE_HUB_UPDATE
|
||||
from .helpers import update_devices
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PulseHub:
|
||||
"""Manages a single Pulse Hub."""
|
||||
|
||||
def __init__(self, hass, config_entry):
|
||||
"""Initialize the system."""
|
||||
self.config_entry = config_entry
|
||||
self.hass = hass
|
||||
self.api: aiopulse2.Hub | None = None
|
||||
self.tasks = []
|
||||
self.current_rollers = {}
|
||||
self.cleanup_callbacks = []
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
"""Return the title of the hub shown in the integrations list."""
|
||||
return f"{self.api.name} ({self.api.host})"
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
"""Return the host of this hub."""
|
||||
return self.config_entry.data["host"]
|
||||
|
||||
async def async_setup(self):
|
||||
"""Set up a hub based on host parameter."""
|
||||
host = self.host
|
||||
|
||||
hub = aiopulse2.Hub(host, propagate_callbacks=True)
|
||||
|
||||
self.api = hub
|
||||
|
||||
hub.callback_subscribe(self.async_notify_update)
|
||||
self.tasks.append(asyncio.create_task(hub.run()))
|
||||
|
||||
_LOGGER.debug("Hub setup complete")
|
||||
return True
|
||||
|
||||
async def async_reset(self):
|
||||
"""Reset this hub to default state."""
|
||||
for cleanup_callback in self.cleanup_callbacks:
|
||||
cleanup_callback()
|
||||
|
||||
# If not setup
|
||||
if self.api is None:
|
||||
return False
|
||||
|
||||
self.api.callback_unsubscribe(self.async_notify_update)
|
||||
await self.api.stop()
|
||||
del self.api
|
||||
self.api = None
|
||||
|
||||
# Wait for any running tasks to complete
|
||||
await asyncio.wait(self.tasks)
|
||||
|
||||
return True
|
||||
|
||||
async def async_notify_update(self, hub=None):
|
||||
"""Evaluate entities when hub reports that update has occurred."""
|
||||
_LOGGER.debug("Hub {self.title} updated")
|
||||
|
||||
await update_devices(self.hass, self.config_entry, self.api.rollers)
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, title=self.title)
|
||||
|
||||
async_dispatcher_send(
|
||||
self.hass, AUTOMATE_HUB_UPDATE.format(self.config_entry.entry_id)
|
||||
)
|
||||
|
||||
for unique_id in list(self.current_rollers):
|
||||
if unique_id not in self.api.rollers:
|
||||
_LOGGER.debug("Notifying remove of %s", unique_id)
|
||||
self.current_rollers.pop(unique_id)
|
||||
async_dispatcher_send(
|
||||
self.hass, AUTOMATE_ENTITY_REMOVE.format(unique_id)
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"domain": "automate",
|
||||
"name": "Automate Pulse Hub v2",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"documentation": "https://www.home-assistant.io/integrations/automate",
|
||||
"requirements": [
|
||||
"aiopulse2==0.6.0"
|
||||
],
|
||||
"codeowners": [
|
||||
"@sillyfrog"
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "El dispositiu ja est\u00e0 configurat"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Ha fallat la connexi\u00f3",
|
||||
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
|
||||
"unknown": "Error inesperat"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Amfitri\u00f3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
"invalid_auth": "Ung\u00fcltige Authentifizierung",
|
||||
"unknown": "Unerwarteter Fehler"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Seade on juba h\u00e4\u00e4lestatud"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00dchendamine nurjus",
|
||||
"invalid_auth": "Tuvastamine nurjus",
|
||||
"unknown": "Ootamatu t\u00f5rge"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
|
||||
"invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
|
||||
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "\u05de\u05d0\u05e8\u05d7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Apparaat is al geconfigureerd"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan geen verbinding maken",
|
||||
"invalid_auth": "Ongeldige authenticatie",
|
||||
"unknown": "Onverwachte fout"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
|
||||
"invalid_auth": "Niepoprawne uwierzytelnienie",
|
||||
"unknown": "Nieoczekiwany b\u0142\u0105d"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Nazwa hosta lub adres IP"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
|
||||
"invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
|
||||
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "\u0425\u043e\u0441\u0442"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u9023\u7dda\u5931\u6557",
|
||||
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
|
||||
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "\u4e3b\u6a5f\u7aef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"requirements": ["bimmer_connected==0.7.15"],
|
||||
"requirements": ["bimmer_connected==0.7.16"],
|
||||
"codeowners": ["@gerard33", "@rikroe"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_VOLTAGE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
DEVICE_CLASS_POWER,
|
||||
@@ -40,6 +41,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR
|
||||
from .deconz_device import DeconzDevice
|
||||
@@ -51,6 +53,7 @@ ATTR_DAYLIGHT = "daylight"
|
||||
ATTR_EVENT_ID = "event_id"
|
||||
|
||||
DEVICE_CLASS = {
|
||||
Consumption: DEVICE_CLASS_ENERGY,
|
||||
Humidity: DEVICE_CLASS_HUMIDITY,
|
||||
LightLevel: DEVICE_CLASS_ILLUMINANCE,
|
||||
Power: DEVICE_CLASS_POWER,
|
||||
@@ -65,6 +68,7 @@ ICON = {
|
||||
}
|
||||
|
||||
STATE_CLASS = {
|
||||
Consumption: STATE_CLASS_MEASUREMENT,
|
||||
Humidity: STATE_CLASS_MEASUREMENT,
|
||||
Pressure: STATE_CLASS_MEASUREMENT,
|
||||
Temperature: STATE_CLASS_MEASUREMENT,
|
||||
@@ -158,6 +162,9 @@ class DeconzSensor(DeconzDevice, SensorEntity):
|
||||
self._attr_state_class = STATE_CLASS.get(type(self._device))
|
||||
self._attr_unit_of_measurement = UNIT_OF_MEASUREMENT.get(type(self._device))
|
||||
|
||||
if device.type in Consumption.ZHATYPE:
|
||||
self._attr_last_reset = dt_util.utc_from_timestamp(0)
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update=False):
|
||||
"""Update the sensor's state."""
|
||||
|
||||
@@ -165,6 +165,9 @@ async def async_setup_entry(
|
||||
LOGGER.exception("Error connecting to DSMR")
|
||||
transport = None
|
||||
protocol = None
|
||||
|
||||
# throttle reconnect attempts
|
||||
await asyncio.sleep(entry.data[CONF_RECONNECT_INTERVAL])
|
||||
except CancelledError:
|
||||
if stop_listener:
|
||||
stop_listener() # pylint: disable=not-callable
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,39 +4,27 @@ from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .definitions import DEFINITIONS
|
||||
from .definitions import SENSORS, DSMRReaderSensorEntityDescription
|
||||
|
||||
DOMAIN = "dsmr_reader"
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up DSMR Reader sensors."""
|
||||
|
||||
sensors = []
|
||||
for topic in DEFINITIONS:
|
||||
sensors.append(DSMRSensor(topic))
|
||||
|
||||
async_add_entities(sensors)
|
||||
async_add_entities(DSMRSensor(description) for description in SENSORS)
|
||||
|
||||
|
||||
class DSMRSensor(SensorEntity):
|
||||
"""Representation of a DSMR sensor that is updated via MQTT."""
|
||||
|
||||
def __init__(self, topic):
|
||||
entity_description: DSMRReaderSensorEntityDescription
|
||||
|
||||
def __init__(self, description: DSMRReaderSensorEntityDescription) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
|
||||
self._definition = DEFINITIONS[topic]
|
||||
|
||||
self._entity_id = slugify(topic.replace("/", "_"))
|
||||
self._topic = topic
|
||||
|
||||
self._name = self._definition.get("name", topic.split("/")[-1])
|
||||
self._device_class = self._definition.get("device_class")
|
||||
self._enable_default = self._definition.get("enable_default")
|
||||
self._unit_of_measurement = self._definition.get("unit")
|
||||
self._icon = self._definition.get("icon")
|
||||
self._transform = self._definition.get("transform")
|
||||
self._state = None
|
||||
slug = slugify(description.key.replace("/", "_"))
|
||||
self.entity_id = f"sensor.{slug}"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to MQTT events."""
|
||||
@@ -44,47 +32,13 @@ class DSMRSensor(SensorEntity):
|
||||
@callback
|
||||
def message_received(message):
|
||||
"""Handle new MQTT messages."""
|
||||
|
||||
if self._transform is not None:
|
||||
self._state = self._transform(message.payload)
|
||||
if self.entity_description.state is not None:
|
||||
self._attr_state = self.entity_description.state(message.payload)
|
||||
else:
|
||||
self._state = message.payload
|
||||
self._attr_state = message.payload
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
await mqtt.async_subscribe(self.hass, self._topic, message_received, 1)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor supplied in constructor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""Return the entity ID for this sensor."""
|
||||
return f"sensor.{self._entity_id}"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state of the entity."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device_class of this sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit_of_measurement of this sensor."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self._enable_default
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of this sensor."""
|
||||
return self._icon
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self.entity_description.key, message_received, 1
|
||||
)
|
||||
|
||||
@@ -152,10 +152,12 @@ class EnergyCostSensor(SensorEntity):
|
||||
self._attr_state_class = STATE_CLASS_MEASUREMENT
|
||||
self._flow = flow
|
||||
self._last_energy_sensor_state: State | None = None
|
||||
self._cur_value = 0.0
|
||||
|
||||
def _reset(self, energy_state: State) -> None:
|
||||
"""Reset the cost sensor."""
|
||||
self._attr_state = 0.0
|
||||
self._cur_value = 0.0
|
||||
self._attr_last_reset = dt_util.utcnow()
|
||||
self._last_energy_sensor_state = energy_state
|
||||
self.async_write_ha_state()
|
||||
@@ -195,7 +197,6 @@ class EnergyCostSensor(SensorEntity):
|
||||
self._reset(energy_state)
|
||||
return
|
||||
|
||||
cur_value = cast(float, self._attr_state)
|
||||
if (
|
||||
energy_state.attributes[ATTR_LAST_RESET]
|
||||
!= self._last_energy_sensor_state.attributes[ATTR_LAST_RESET]
|
||||
@@ -205,7 +206,8 @@ class EnergyCostSensor(SensorEntity):
|
||||
else:
|
||||
# Update with newly incurred cost
|
||||
old_energy_value = float(self._last_energy_sensor_state.state)
|
||||
self._attr_state = cur_value + (energy - old_energy_value) * energy_price
|
||||
self._cur_value += (energy - old_energy_value) * energy_price
|
||||
self._attr_state = round(self._cur_value, 2)
|
||||
|
||||
self._last_energy_sensor_state = energy_state
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ async def async_setup_entry(
|
||||
ATTR_ENTITY_ID: f"{device.ain}_total_energy",
|
||||
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
|
||||
ATTR_STATE_CLASS: None,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
coordinator,
|
||||
ain,
|
||||
|
||||
@@ -8,17 +8,26 @@ import logging
|
||||
from pyfronius import Fronius
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE,
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
CONF_RESOURCE,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_SENSOR_TYPE,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.util import dt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -152,6 +161,12 @@ class FroniusAdapter:
|
||||
"""Whether the fronius device is active."""
|
||||
return self._available
|
||||
|
||||
def entity_description( # pylint: disable=no-self-use
|
||||
self, key
|
||||
) -> SensorEntityDescription | None:
|
||||
"""Create entity description for a key."""
|
||||
return None
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve and update latest state."""
|
||||
try:
|
||||
@@ -198,14 +213,28 @@ class FroniusAdapter:
|
||||
async def _update(self) -> dict:
|
||||
"""Return values of interest."""
|
||||
|
||||
async def register(self, sensor):
|
||||
@callback
|
||||
def register(self, sensor):
|
||||
"""Register child sensor for update subscriptions."""
|
||||
self._registered_sensors.add(sensor)
|
||||
return lambda: self._registered_sensors.remove(sensor)
|
||||
|
||||
|
||||
class FroniusInverterSystem(FroniusAdapter):
|
||||
"""Adapter for the fronius inverter with system scope."""
|
||||
|
||||
def entity_description(self, key):
|
||||
"""Return the entity descriptor."""
|
||||
if key != "energy_total":
|
||||
return None
|
||||
|
||||
return SensorEntityDescription(
|
||||
key=key,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
last_reset=dt.utc_from_timestamp(0),
|
||||
)
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.bridge.current_system_inverter_data()
|
||||
@@ -214,6 +243,18 @@ class FroniusInverterSystem(FroniusAdapter):
|
||||
class FroniusInverterDevice(FroniusAdapter):
|
||||
"""Adapter for the fronius inverter with device scope."""
|
||||
|
||||
def entity_description(self, key):
|
||||
"""Return the entity descriptor."""
|
||||
if key != "energy_total":
|
||||
return None
|
||||
|
||||
return SensorEntityDescription(
|
||||
key=key,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
last_reset=dt.utc_from_timestamp(0),
|
||||
)
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.bridge.current_inverter_data(self._device)
|
||||
@@ -230,6 +271,18 @@ class FroniusStorage(FroniusAdapter):
|
||||
class FroniusMeterSystem(FroniusAdapter):
|
||||
"""Adapter for the fronius meter with system scope."""
|
||||
|
||||
def entity_description(self, key):
|
||||
"""Return the entity descriptor."""
|
||||
if not key.startswith("energy_real_"):
|
||||
return None
|
||||
|
||||
return SensorEntityDescription(
|
||||
key=key,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
last_reset=dt.utc_from_timestamp(0),
|
||||
)
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.bridge.current_system_meter_data()
|
||||
@@ -238,6 +291,18 @@ class FroniusMeterSystem(FroniusAdapter):
|
||||
class FroniusMeterDevice(FroniusAdapter):
|
||||
"""Adapter for the fronius meter with device scope."""
|
||||
|
||||
def entity_description(self, key):
|
||||
"""Return the entity descriptor."""
|
||||
if not key.startswith("energy_real_"):
|
||||
return None
|
||||
|
||||
return SensorEntityDescription(
|
||||
key=key,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
last_reset=dt.utc_from_timestamp(0),
|
||||
)
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.bridge.current_meter_data(self._device)
|
||||
@@ -246,6 +311,14 @@ class FroniusMeterDevice(FroniusAdapter):
|
||||
class FroniusPowerFlow(FroniusAdapter):
|
||||
"""Adapter for the fronius power flow."""
|
||||
|
||||
def entity_description(self, key):
|
||||
"""Return the entity descriptor."""
|
||||
return SensorEntityDescription(
|
||||
key=key,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
async def _update(self):
|
||||
"""Get the values for the current state."""
|
||||
return await self.bridge.current_power_flow()
|
||||
@@ -254,27 +327,13 @@ class FroniusPowerFlow(FroniusAdapter):
|
||||
class FroniusTemplateSensor(SensorEntity):
|
||||
"""Sensor for the single values (e.g. pv power, ac power)."""
|
||||
|
||||
def __init__(self, parent: FroniusAdapter, name):
|
||||
def __init__(self, parent: FroniusAdapter, key):
|
||||
"""Initialize a singular value sensor."""
|
||||
self._name = name
|
||||
self.parent = parent
|
||||
self._state = None
|
||||
self._unit = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._name.replace('_', ' ').capitalize()} {self.parent.name}"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
self._key = key
|
||||
self._attr_name = f"{key.replace('_', ' ').capitalize()} {parent.name}"
|
||||
self._parent = parent
|
||||
if entity_description := parent.entity_description(key):
|
||||
self.entity_description = entity_description
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -284,19 +343,19 @@ class FroniusTemplateSensor(SensorEntity):
|
||||
@property
|
||||
def available(self):
|
||||
"""Whether the fronius device is active."""
|
||||
return self.parent.available
|
||||
return self._parent.available
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the internal state."""
|
||||
state = self.parent.data.get(self._name)
|
||||
self._state = state.get("value")
|
||||
if isinstance(self._state, float):
|
||||
self._state = round(self._state, 2)
|
||||
self._unit = state.get("unit")
|
||||
state = self._parent.data.get(self._key)
|
||||
self._attr_state = state.get("value")
|
||||
if isinstance(self._attr_state, float):
|
||||
self._attr_state = round(self._attr_state, 2)
|
||||
self._attr_unit_of_measurement = state.get("unit")
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register at parent component for updates."""
|
||||
await self.parent.register(self)
|
||||
self.async_on_remove(self._parent.register(self))
|
||||
|
||||
def __hash__(self):
|
||||
"""Hash sensor by hashing its name."""
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20210728.0"
|
||||
"home-assistant-frontend==20210729.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
||||
@@ -26,6 +26,7 @@ from .const import (
|
||||
CONF_UID,
|
||||
DATA_CLIENT,
|
||||
DATA_COORDINATOR,
|
||||
DATA_COORDINATOR_PAIRED_SENSOR,
|
||||
DATA_PAIRED_SENSOR_MANAGER,
|
||||
DATA_UNSUB_DISPATCHER_CONNECT,
|
||||
DOMAIN,
|
||||
@@ -44,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
{
|
||||
DATA_CLIENT: {},
|
||||
DATA_COORDINATOR: {},
|
||||
DATA_COORDINATOR_PAIRED_SENSOR: {},
|
||||
DATA_PAIRED_SENSOR_MANAGER: {},
|
||||
DATA_UNSUB_DISPATCHER_CONNECT: {},
|
||||
},
|
||||
@@ -51,9 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
client = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = Client(
|
||||
entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]
|
||||
)
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {
|
||||
API_SENSOR_PAIRED_SENSOR_STATUS: {}
|
||||
}
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {}
|
||||
hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id] = {}
|
||||
hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id] = []
|
||||
|
||||
# The valve controller's UDP-based API can't handle concurrent requests very well,
|
||||
@@ -113,6 +114,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id)
|
||||
hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id)
|
||||
hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR].pop(entry.entry_id)
|
||||
for unsub in hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id]:
|
||||
unsub()
|
||||
hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT].pop(entry.entry_id)
|
||||
@@ -143,8 +145,8 @@ class PairedSensorManager:
|
||||
|
||||
self._paired_uids.add(uid)
|
||||
|
||||
coordinator = self._hass.data[DOMAIN][DATA_COORDINATOR][self._entry.entry_id][
|
||||
API_SENSOR_PAIRED_SENSOR_STATUS
|
||||
coordinator = self._hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][
|
||||
self._entry.entry_id
|
||||
][uid] = GuardianDataUpdateCoordinator(
|
||||
self._hass,
|
||||
client=self._client,
|
||||
@@ -194,8 +196,8 @@ class PairedSensorManager:
|
||||
|
||||
# Clear out objects related to this paired sensor:
|
||||
self._paired_uids.remove(uid)
|
||||
self._hass.data[DOMAIN][DATA_COORDINATOR][self._entry.entry_id][
|
||||
API_SENSOR_PAIRED_SENSOR_STATUS
|
||||
self._hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][
|
||||
self._entry.entry_id
|
||||
].pop(uid)
|
||||
|
||||
# Remove the paired sensor device from the device registry (which will
|
||||
@@ -297,7 +299,6 @@ class ValveControllerEntity(GuardianEntity):
|
||||
return any(
|
||||
coordinator.last_update_success
|
||||
for coordinator in self.coordinators.values()
|
||||
if coordinator
|
||||
)
|
||||
|
||||
async def _async_continue_entity_setup(self) -> None:
|
||||
|
||||
@@ -15,11 +15,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import PairedSensorEntity, ValveControllerEntity
|
||||
from .const import (
|
||||
API_SENSOR_PAIRED_SENSOR_STATUS,
|
||||
API_SYSTEM_ONBOARD_SENSOR_STATUS,
|
||||
API_WIFI_STATUS,
|
||||
CONF_UID,
|
||||
DATA_COORDINATOR,
|
||||
DATA_COORDINATOR_PAIRED_SENSOR,
|
||||
DATA_UNSUB_DISPATCHER_CONNECT,
|
||||
DOMAIN,
|
||||
SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED,
|
||||
@@ -49,9 +49,9 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def add_new_paired_sensor(uid: str) -> None:
|
||||
"""Add a new paired sensor."""
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][
|
||||
API_SENSOR_PAIRED_SENSOR_STATUS
|
||||
][uid]
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id][
|
||||
uid
|
||||
]
|
||||
|
||||
entities = []
|
||||
for kind in PAIRED_SENSOR_SENSORS:
|
||||
@@ -95,8 +95,8 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
# Add all paired sensor-specific binary sensors:
|
||||
for coordinator in hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][
|
||||
API_SENSOR_PAIRED_SENSOR_STATUS
|
||||
for coordinator in hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][
|
||||
entry.entry_id
|
||||
].values():
|
||||
for kind in PAIRED_SENSOR_SENSORS:
|
||||
name, device_class = SENSOR_ATTRS_MAP[kind]
|
||||
|
||||
@@ -16,6 +16,7 @@ CONF_UID = "uid"
|
||||
|
||||
DATA_CLIENT = "client"
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
DATA_COORDINATOR_PAIRED_SENSOR = "coordinator_paired_sensor"
|
||||
DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager"
|
||||
DATA_UNSUB_DISPATCHER_CONNECT = "unsub_dispatcher_connect"
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import PairedSensorEntity, ValveControllerEntity
|
||||
from .const import (
|
||||
API_SENSOR_PAIRED_SENSOR_STATUS,
|
||||
API_SYSTEM_DIAGNOSTICS,
|
||||
API_SYSTEM_ONBOARD_SENSOR_STATUS,
|
||||
CONF_UID,
|
||||
DATA_COORDINATOR,
|
||||
DATA_COORDINATOR_PAIRED_SENSOR,
|
||||
DATA_UNSUB_DISPATCHER_CONNECT,
|
||||
DOMAIN,
|
||||
SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED,
|
||||
@@ -54,9 +54,9 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def add_new_paired_sensor(uid: str) -> None:
|
||||
"""Add a new paired sensor."""
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][
|
||||
API_SENSOR_PAIRED_SENSOR_STATUS
|
||||
][uid]
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id][
|
||||
uid
|
||||
]
|
||||
|
||||
entities = []
|
||||
for kind in PAIRED_SENSOR_SENSORS:
|
||||
@@ -96,8 +96,8 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
# Add all paired sensor-specific binary sensors:
|
||||
for coordinator in hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][
|
||||
API_SENSOR_PAIRED_SENSOR_STATUS
|
||||
for coordinator in hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][
|
||||
entry.entry_id
|
||||
].values():
|
||||
for kind in PAIRED_SENSOR_SENSORS:
|
||||
name, device_class, icon, unit = SENSOR_ATTRS_MAP[kind]
|
||||
|
||||
@@ -4,8 +4,16 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_LAST_RESET,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
PLATFORM_SCHEMA,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_METHOD,
|
||||
CONF_NAME,
|
||||
@@ -20,6 +28,7 @@ from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
# mypy: allow-untyped-defs, no-check-untyped-defs
|
||||
|
||||
@@ -115,16 +124,26 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
|
||||
self._unit_prefix = UNIT_PREFIXES[unit_prefix]
|
||||
self._unit_time = UNIT_TIME[unit_time]
|
||||
self._attr_state_class = STATE_CLASS_MEASUREMENT
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle entity which will be added."""
|
||||
await super().async_added_to_hass()
|
||||
state = await self.async_get_last_state()
|
||||
self._attr_last_reset = dt_util.utcnow()
|
||||
if state:
|
||||
try:
|
||||
self._state = Decimal(state.state)
|
||||
except ValueError as err:
|
||||
except (DecimalException, ValueError) as err:
|
||||
_LOGGER.warning("Could not restore last state: %s", err)
|
||||
else:
|
||||
last_reset = dt_util.parse_datetime(
|
||||
state.attributes.get(ATTR_LAST_RESET, "")
|
||||
)
|
||||
self._attr_last_reset = (
|
||||
last_reset if last_reset else dt_util.utc_from_timestamp(0)
|
||||
)
|
||||
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
|
||||
@callback
|
||||
def calc_integration(event):
|
||||
@@ -143,7 +162,11 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
self._unit_of_measurement = self._unit_template.format(
|
||||
"" if unit is None else unit
|
||||
)
|
||||
|
||||
if (
|
||||
self.device_class is None
|
||||
and new_state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
|
||||
):
|
||||
self._attr_device_class = DEVICE_CLASS_ENERGY
|
||||
try:
|
||||
# integration as the Riemann integral of previous measures.
|
||||
area = 0
|
||||
|
||||
@@ -296,11 +296,7 @@ turn_on:
|
||||
name: Effect
|
||||
description: Light effect.
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- colorloop
|
||||
- random
|
||||
- white
|
||||
text:
|
||||
|
||||
turn_off:
|
||||
name: Turn off
|
||||
|
||||
@@ -28,7 +28,6 @@ from homeassistant.helpers.update_coordinator import (
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
from homeassistant.util.async_ import gather_with_concurrency
|
||||
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN, SERVICES
|
||||
|
||||
@@ -143,14 +142,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
try:
|
||||
vehicles = await with_timeout(mazda_client.get_vehicles())
|
||||
|
||||
vehicle_status_tasks = [
|
||||
with_timeout(mazda_client.get_vehicle_status(vehicle["id"]))
|
||||
for vehicle in vehicles
|
||||
]
|
||||
statuses = await gather_with_concurrency(5, *vehicle_status_tasks)
|
||||
|
||||
for vehicle, status in zip(vehicles, statuses):
|
||||
vehicle["status"] = status
|
||||
# The Mazda API can throw an error when multiple simultaneous requests are
|
||||
# made for the same account, so we can only make one request at a time here
|
||||
for vehicle in vehicles:
|
||||
vehicle["status"] = await with_timeout(
|
||||
mazda_client.get_vehicle_status(vehicle["id"])
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ class MqttSensor(MqttEntity, SensorEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
if CONF_LAST_RESET_TOPIC in self._config:
|
||||
topics["state_topic"] = {
|
||||
topics["last_reset_topic"] = {
|
||||
"topic": self._config[CONF_LAST_RESET_TOPIC],
|
||||
"msg_callback": last_reset_message_received,
|
||||
"qos": self._config[CONF_QOS],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["ffmpeg", "http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/nest",
|
||||
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.4"],
|
||||
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.5"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"quality_scale": "platinum",
|
||||
"dhcp": [
|
||||
|
||||
@@ -3,10 +3,10 @@ import logging
|
||||
|
||||
from pyprosegur.auth import Auth
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import CONF_COUNTRY, DOMAIN
|
||||
@@ -32,12 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except ConnectionRefusedError as error:
|
||||
_LOGGER.error("Configured credential are invalid, %s", error)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_REAUTH, "entry_id": entry.data["entry_id"]},
|
||||
)
|
||||
)
|
||||
raise ConfigEntryAuthFailed from error
|
||||
|
||||
except ConnectionError as error:
|
||||
_LOGGER.error("Could not connect with Prosegur backend: %s", error)
|
||||
|
||||
@@ -25,11 +25,9 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect."""
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
auth = Auth(session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_COUNTRY])
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
auth = Auth(
|
||||
session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_COUNTRY]
|
||||
)
|
||||
install = await Installation.retrieve(auth)
|
||||
except ConnectionRefusedError:
|
||||
raise InvalidAuth from ConnectionRefusedError
|
||||
@@ -95,15 +93,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception as exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(exception)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
data = self.entry.data.copy()
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.entry,
|
||||
data={
|
||||
**data,
|
||||
**self.entry.data,
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
},
|
||||
|
||||
@@ -92,3 +92,6 @@ KELVIN_MIN_VALUE_WHITE: Final = 2700
|
||||
KELVIN_MIN_VALUE_COLOR: Final = 3000
|
||||
|
||||
UPTIME_DEVIATION: Final = 5
|
||||
|
||||
LAST_RESET_UPTIME: Final = "uptime"
|
||||
LAST_RESET_NEVER: Final = "never"
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any, Callable, Final, cast
|
||||
|
||||
@@ -180,7 +179,7 @@ class BlockAttributeDescription:
|
||||
# Callable (settings, block), return true if entity should be removed
|
||||
removal_condition: Callable[[dict, aioshelly.Block], bool] | None = None
|
||||
extra_state_attributes: Callable[[aioshelly.Block], dict | None] | None = None
|
||||
last_reset: datetime | None = None
|
||||
last_reset: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -286,6 +285,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
|
||||
self._unit: None | str | Callable[[dict], str] = unit
|
||||
self._unique_id: str = f"{super().unique_id}-{self.attribute}"
|
||||
self._name = get_entity_name(wrapper.device, block, self.description.name)
|
||||
self._last_value: str | None = None
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util import dt
|
||||
|
||||
from .const import SHAIR_MAX_WORK_HOURS
|
||||
from .const import LAST_RESET_NEVER, LAST_RESET_UPTIME, SHAIR_MAX_WORK_HOURS
|
||||
from .entity import (
|
||||
BlockAttributeDescription,
|
||||
RestAttributeDescription,
|
||||
@@ -114,6 +114,7 @@ SENSORS: Final = {
|
||||
value=lambda value: round(value / 60 / 1000, 2),
|
||||
device_class=sensor.DEVICE_CLASS_ENERGY,
|
||||
state_class=sensor.STATE_CLASS_MEASUREMENT,
|
||||
last_reset=LAST_RESET_UPTIME,
|
||||
),
|
||||
("emeter", "energy"): BlockAttributeDescription(
|
||||
name="Energy",
|
||||
@@ -121,7 +122,7 @@ SENSORS: Final = {
|
||||
value=lambda value: round(value / 1000, 2),
|
||||
device_class=sensor.DEVICE_CLASS_ENERGY,
|
||||
state_class=sensor.STATE_CLASS_MEASUREMENT,
|
||||
last_reset=dt.utc_from_timestamp(0),
|
||||
last_reset=LAST_RESET_NEVER,
|
||||
),
|
||||
("emeter", "energyReturned"): BlockAttributeDescription(
|
||||
name="Energy Returned",
|
||||
@@ -129,7 +130,7 @@ SENSORS: Final = {
|
||||
value=lambda value: round(value / 1000, 2),
|
||||
device_class=sensor.DEVICE_CLASS_ENERGY,
|
||||
state_class=sensor.STATE_CLASS_MEASUREMENT,
|
||||
last_reset=dt.utc_from_timestamp(0),
|
||||
last_reset=LAST_RESET_NEVER,
|
||||
),
|
||||
("light", "energy"): BlockAttributeDescription(
|
||||
name="Energy",
|
||||
@@ -138,6 +139,7 @@ SENSORS: Final = {
|
||||
device_class=sensor.DEVICE_CLASS_ENERGY,
|
||||
state_class=sensor.STATE_CLASS_MEASUREMENT,
|
||||
default_enabled=False,
|
||||
last_reset=LAST_RESET_UPTIME,
|
||||
),
|
||||
("relay", "energy"): BlockAttributeDescription(
|
||||
name="Energy",
|
||||
@@ -145,6 +147,7 @@ SENSORS: Final = {
|
||||
value=lambda value: round(value / 60 / 1000, 2),
|
||||
device_class=sensor.DEVICE_CLASS_ENERGY,
|
||||
state_class=sensor.STATE_CLASS_MEASUREMENT,
|
||||
last_reset=LAST_RESET_UPTIME,
|
||||
),
|
||||
("roller", "rollerEnergy"): BlockAttributeDescription(
|
||||
name="Energy",
|
||||
@@ -152,6 +155,7 @@ SENSORS: Final = {
|
||||
value=lambda value: round(value / 60 / 1000, 2),
|
||||
device_class=sensor.DEVICE_CLASS_ENERGY,
|
||||
state_class=sensor.STATE_CLASS_MEASUREMENT,
|
||||
last_reset=LAST_RESET_UPTIME,
|
||||
),
|
||||
("sensor", "concentration"): BlockAttributeDescription(
|
||||
name="Gas Concentration",
|
||||
@@ -264,7 +268,16 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity):
|
||||
@property
|
||||
def last_reset(self) -> datetime | None:
|
||||
"""State class of sensor."""
|
||||
return self.description.last_reset
|
||||
if self.description.last_reset == LAST_RESET_UPTIME:
|
||||
self._last_value = get_device_uptime(
|
||||
self.wrapper.device.status, self._last_value
|
||||
)
|
||||
return dt.parse_datetime(self._last_value)
|
||||
|
||||
if self.description.last_reset == LAST_RESET_NEVER:
|
||||
return dt.utc_from_timestamp(0)
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str | None:
|
||||
|
||||
@@ -136,7 +136,7 @@ def is_momentary_input(settings: dict[str, Any], block: aioshelly.Block) -> bool
|
||||
return button_type in ["momentary", "momentary_on_release"]
|
||||
|
||||
|
||||
def get_device_uptime(status: dict[str, Any], last_uptime: str) -> str:
|
||||
def get_device_uptime(status: dict[str, Any], last_uptime: str | None) -> str:
|
||||
"""Return device uptime string, tolerate up to 5 seconds deviation."""
|
||||
delta_uptime = utcnow() - timedelta(seconds=status["uptime"])
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import logging
|
||||
from aiohttp.client_exceptions import ClientConnectionError, ClientResponseError
|
||||
from pysmartapp.event import EVENT_TYPE_DEVICE
|
||||
from pysmartthings import Attribute, Capability, SmartThings
|
||||
from pysmartthings.device import DeviceEntity
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -412,7 +413,7 @@ class DeviceBroker:
|
||||
class SmartThingsEntity(Entity):
|
||||
"""Defines a SmartThings entity."""
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, device: DeviceEntity) -> None:
|
||||
"""Initialize the instance."""
|
||||
self._device = device
|
||||
self._dispatcher_remove = None
|
||||
|
||||
@@ -3,18 +3,26 @@ from __future__ import annotations
|
||||
|
||||
from collections import namedtuple
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
|
||||
from pysmartthings import Attribute, Capability
|
||||
from pysmartthings.device import DeviceEntity
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
|
||||
from homeassistant.const import (
|
||||
AREA_SQUARE_METERS,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CO,
|
||||
DEVICE_CLASS_CO2,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ELECTRIC_POTENTIAL_VOLT,
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
LIGHT_LUX,
|
||||
@@ -25,26 +33,27 @@ from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
VOLUME_CUBIC_METERS,
|
||||
)
|
||||
from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
from . import SmartThingsEntity
|
||||
from .const import DATA_BROKERS, DOMAIN
|
||||
|
||||
Map = namedtuple("map", "attribute name default_unit device_class")
|
||||
Map = namedtuple("map", "attribute name default_unit device_class state_class")
|
||||
|
||||
CAPABILITY_TO_SENSORS = {
|
||||
Capability.activity_lighting_mode: [
|
||||
Map(Attribute.lighting_mode, "Activity Lighting Mode", None, None)
|
||||
Map(Attribute.lighting_mode, "Activity Lighting Mode", None, None, None)
|
||||
],
|
||||
Capability.air_conditioner_mode: [
|
||||
Map(Attribute.air_conditioner_mode, "Air Conditioner Mode", None, None)
|
||||
Map(Attribute.air_conditioner_mode, "Air Conditioner Mode", None, None, None)
|
||||
],
|
||||
Capability.air_quality_sensor: [
|
||||
Map(Attribute.air_quality, "Air Quality", "CAQI", None)
|
||||
Map(Attribute.air_quality, "Air Quality", "CAQI", None, STATE_CLASS_MEASUREMENT)
|
||||
],
|
||||
Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None)],
|
||||
Capability.audio_volume: [Map(Attribute.volume, "Volume", PERCENTAGE, None)],
|
||||
Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None, None)],
|
||||
Capability.audio_volume: [Map(Attribute.volume, "Volume", PERCENTAGE, None, None)],
|
||||
Capability.battery: [
|
||||
Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY)
|
||||
Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY, None)
|
||||
],
|
||||
Capability.body_mass_index_measurement: [
|
||||
Map(
|
||||
@@ -52,57 +61,80 @@ CAPABILITY_TO_SENSORS = {
|
||||
"Body Mass Index",
|
||||
f"{MASS_KILOGRAMS}/{AREA_SQUARE_METERS}",
|
||||
None,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.body_weight_measurement: [
|
||||
Map(Attribute.body_weight_measurement, "Body Weight", MASS_KILOGRAMS, None)
|
||||
Map(
|
||||
Attribute.body_weight_measurement,
|
||||
"Body Weight",
|
||||
MASS_KILOGRAMS,
|
||||
None,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.carbon_dioxide_measurement: [
|
||||
Map(
|
||||
Attribute.carbon_dioxide,
|
||||
"Carbon Dioxide Measurement",
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
None,
|
||||
DEVICE_CLASS_CO2,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.carbon_monoxide_detector: [
|
||||
Map(Attribute.carbon_monoxide, "Carbon Monoxide Detector", None, None)
|
||||
Map(Attribute.carbon_monoxide, "Carbon Monoxide Detector", None, None, None)
|
||||
],
|
||||
Capability.carbon_monoxide_measurement: [
|
||||
Map(
|
||||
Attribute.carbon_monoxide_level,
|
||||
"Carbon Monoxide Measurement",
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
None,
|
||||
DEVICE_CLASS_CO,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.dishwasher_operating_state: [
|
||||
Map(Attribute.machine_state, "Dishwasher Machine State", None, None),
|
||||
Map(Attribute.dishwasher_job_state, "Dishwasher Job State", None, None),
|
||||
Map(Attribute.machine_state, "Dishwasher Machine State", None, None, None),
|
||||
Map(Attribute.dishwasher_job_state, "Dishwasher Job State", None, None, None),
|
||||
Map(
|
||||
Attribute.completion_time,
|
||||
"Dishwasher Completion Time",
|
||||
None,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
None,
|
||||
),
|
||||
],
|
||||
Capability.dryer_mode: [Map(Attribute.dryer_mode, "Dryer Mode", None, None)],
|
||||
Capability.dryer_mode: [Map(Attribute.dryer_mode, "Dryer Mode", None, None, None)],
|
||||
Capability.dryer_operating_state: [
|
||||
Map(Attribute.machine_state, "Dryer Machine State", None, None),
|
||||
Map(Attribute.dryer_job_state, "Dryer Job State", None, None),
|
||||
Map(Attribute.machine_state, "Dryer Machine State", None, None, None),
|
||||
Map(Attribute.dryer_job_state, "Dryer Job State", None, None, None),
|
||||
Map(
|
||||
Attribute.completion_time,
|
||||
"Dryer Completion Time",
|
||||
None,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
None,
|
||||
),
|
||||
],
|
||||
Capability.dust_sensor: [
|
||||
Map(Attribute.fine_dust_level, "Fine Dust Level", None, None),
|
||||
Map(Attribute.dust_level, "Dust Level", None, None),
|
||||
Map(
|
||||
Attribute.fine_dust_level,
|
||||
"Fine Dust Level",
|
||||
None,
|
||||
None,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
Map(Attribute.dust_level, "Dust Level", None, None, STATE_CLASS_MEASUREMENT),
|
||||
],
|
||||
Capability.energy_meter: [
|
||||
Map(Attribute.energy, "Energy Meter", ENERGY_KILO_WATT_HOUR, None)
|
||||
Map(
|
||||
Attribute.energy,
|
||||
"Energy Meter",
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.equivalent_carbon_dioxide_measurement: [
|
||||
Map(
|
||||
@@ -110,6 +142,7 @@ CAPABILITY_TO_SENSORS = {
|
||||
"Equivalent Carbon Dioxide Measurement",
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
None,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.formaldehyde_measurement: [
|
||||
@@ -118,50 +151,94 @@ CAPABILITY_TO_SENSORS = {
|
||||
"Formaldehyde Measurement",
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
None,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.gas_meter: [
|
||||
Map(Attribute.gas_meter, "Gas Meter", ENERGY_KILO_WATT_HOUR, None),
|
||||
Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None),
|
||||
Map(Attribute.gas_meter_time, "Gas Meter Time", None, DEVICE_CLASS_TIMESTAMP),
|
||||
Map(Attribute.gas_meter_volume, "Gas Meter Volume", VOLUME_CUBIC_METERS, None),
|
||||
Map(
|
||||
Attribute.gas_meter,
|
||||
"Gas Meter",
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
None,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None, None),
|
||||
Map(
|
||||
Attribute.gas_meter_time,
|
||||
"Gas Meter Time",
|
||||
None,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
None,
|
||||
),
|
||||
Map(
|
||||
Attribute.gas_meter_volume,
|
||||
"Gas Meter Volume",
|
||||
VOLUME_CUBIC_METERS,
|
||||
None,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
],
|
||||
Capability.illuminance_measurement: [
|
||||
Map(Attribute.illuminance, "Illuminance", LIGHT_LUX, DEVICE_CLASS_ILLUMINANCE)
|
||||
Map(
|
||||
Attribute.illuminance,
|
||||
"Illuminance",
|
||||
LIGHT_LUX,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.infrared_level: [
|
||||
Map(Attribute.infrared_level, "Infrared Level", PERCENTAGE, None)
|
||||
Map(
|
||||
Attribute.infrared_level,
|
||||
"Infrared Level",
|
||||
PERCENTAGE,
|
||||
None,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.media_input_source: [
|
||||
Map(Attribute.input_source, "Media Input Source", None, None)
|
||||
Map(Attribute.input_source, "Media Input Source", None, None, None)
|
||||
],
|
||||
Capability.media_playback_repeat: [
|
||||
Map(Attribute.playback_repeat_mode, "Media Playback Repeat", None, None)
|
||||
Map(Attribute.playback_repeat_mode, "Media Playback Repeat", None, None, None)
|
||||
],
|
||||
Capability.media_playback_shuffle: [
|
||||
Map(Attribute.playback_shuffle, "Media Playback Shuffle", None, None)
|
||||
Map(Attribute.playback_shuffle, "Media Playback Shuffle", None, None, None)
|
||||
],
|
||||
Capability.media_playback: [
|
||||
Map(Attribute.playback_status, "Media Playback Status", None, None)
|
||||
Map(Attribute.playback_status, "Media Playback Status", None, None, None)
|
||||
],
|
||||
Capability.odor_sensor: [Map(Attribute.odor_level, "Odor Sensor", None, None)],
|
||||
Capability.oven_mode: [Map(Attribute.oven_mode, "Oven Mode", None, None)],
|
||||
Capability.odor_sensor: [
|
||||
Map(Attribute.odor_level, "Odor Sensor", None, None, None)
|
||||
],
|
||||
Capability.oven_mode: [Map(Attribute.oven_mode, "Oven Mode", None, None, None)],
|
||||
Capability.oven_operating_state: [
|
||||
Map(Attribute.machine_state, "Oven Machine State", None, None),
|
||||
Map(Attribute.oven_job_state, "Oven Job State", None, None),
|
||||
Map(Attribute.completion_time, "Oven Completion Time", None, None),
|
||||
Map(Attribute.machine_state, "Oven Machine State", None, None, None),
|
||||
Map(Attribute.oven_job_state, "Oven Job State", None, None, None),
|
||||
Map(Attribute.completion_time, "Oven Completion Time", None, None, None),
|
||||
],
|
||||
Capability.oven_setpoint: [
|
||||
Map(Attribute.oven_setpoint, "Oven Set Point", None, None)
|
||||
Map(Attribute.oven_setpoint, "Oven Set Point", None, None, None)
|
||||
],
|
||||
Capability.power_meter: [
|
||||
Map(
|
||||
Attribute.power,
|
||||
"Power Meter",
|
||||
POWER_WATT,
|
||||
DEVICE_CLASS_POWER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.power_source: [
|
||||
Map(Attribute.power_source, "Power Source", None, None, None)
|
||||
],
|
||||
Capability.power_meter: [Map(Attribute.power, "Power Meter", POWER_WATT, None)],
|
||||
Capability.power_source: [Map(Attribute.power_source, "Power Source", None, None)],
|
||||
Capability.refrigeration_setpoint: [
|
||||
Map(
|
||||
Attribute.refrigeration_setpoint,
|
||||
"Refrigeration Setpoint",
|
||||
None,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
None,
|
||||
)
|
||||
],
|
||||
Capability.relative_humidity_measurement: [
|
||||
@@ -170,6 +247,7 @@ CAPABILITY_TO_SENSORS = {
|
||||
"Relative Humidity Measurement",
|
||||
PERCENTAGE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.robot_cleaner_cleaning_mode: [
|
||||
@@ -178,25 +256,43 @@ CAPABILITY_TO_SENSORS = {
|
||||
"Robot Cleaner Cleaning Mode",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
],
|
||||
Capability.robot_cleaner_movement: [
|
||||
Map(Attribute.robot_cleaner_movement, "Robot Cleaner Movement", None, None)
|
||||
Map(
|
||||
Attribute.robot_cleaner_movement, "Robot Cleaner Movement", None, None, None
|
||||
)
|
||||
],
|
||||
Capability.robot_cleaner_turbo_mode: [
|
||||
Map(Attribute.robot_cleaner_turbo_mode, "Robot Cleaner Turbo Mode", None, None)
|
||||
Map(
|
||||
Attribute.robot_cleaner_turbo_mode,
|
||||
"Robot Cleaner Turbo Mode",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
],
|
||||
Capability.signal_strength: [
|
||||
Map(Attribute.lqi, "LQI Signal Strength", None, None),
|
||||
Map(Attribute.rssi, "RSSI Signal Strength", None, None),
|
||||
Map(Attribute.lqi, "LQI Signal Strength", None, None, STATE_CLASS_MEASUREMENT),
|
||||
Map(
|
||||
Attribute.rssi,
|
||||
"RSSI Signal Strength",
|
||||
None,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
],
|
||||
Capability.smoke_detector: [
|
||||
Map(Attribute.smoke, "Smoke Detector", None, None, None)
|
||||
],
|
||||
Capability.smoke_detector: [Map(Attribute.smoke, "Smoke Detector", None, None)],
|
||||
Capability.temperature_measurement: [
|
||||
Map(
|
||||
Attribute.temperature,
|
||||
"Temperature Measurement",
|
||||
None,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.thermostat_cooling_setpoint: [
|
||||
@@ -205,10 +301,11 @@ CAPABILITY_TO_SENSORS = {
|
||||
"Thermostat Cooling Setpoint",
|
||||
None,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
None,
|
||||
)
|
||||
],
|
||||
Capability.thermostat_fan_mode: [
|
||||
Map(Attribute.thermostat_fan_mode, "Thermostat Fan Mode", None, None)
|
||||
Map(Attribute.thermostat_fan_mode, "Thermostat Fan Mode", None, None, None)
|
||||
],
|
||||
Capability.thermostat_heating_setpoint: [
|
||||
Map(
|
||||
@@ -216,10 +313,11 @@ CAPABILITY_TO_SENSORS = {
|
||||
"Thermostat Heating Setpoint",
|
||||
None,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
None,
|
||||
)
|
||||
],
|
||||
Capability.thermostat_mode: [
|
||||
Map(Attribute.thermostat_mode, "Thermostat Mode", None, None)
|
||||
Map(Attribute.thermostat_mode, "Thermostat Mode", None, None, None)
|
||||
],
|
||||
Capability.thermostat_operating_state: [
|
||||
Map(
|
||||
@@ -227,6 +325,7 @@ CAPABILITY_TO_SENSORS = {
|
||||
"Thermostat Operating State",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
],
|
||||
Capability.thermostat_setpoint: [
|
||||
@@ -235,12 +334,13 @@ CAPABILITY_TO_SENSORS = {
|
||||
"Thermostat Setpoint",
|
||||
None,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
None,
|
||||
)
|
||||
],
|
||||
Capability.three_axis: [],
|
||||
Capability.tv_channel: [
|
||||
Map(Attribute.tv_channel, "Tv Channel", None, None),
|
||||
Map(Attribute.tv_channel_name, "Tv Channel Name", None, None),
|
||||
Map(Attribute.tv_channel, "Tv Channel", None, None, None),
|
||||
Map(Attribute.tv_channel_name, "Tv Channel Name", None, None, None),
|
||||
],
|
||||
Capability.tvoc_measurement: [
|
||||
Map(
|
||||
@@ -248,23 +348,39 @@ CAPABILITY_TO_SENSORS = {
|
||||
"Tvoc Measurement",
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
None,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.ultraviolet_index: [
|
||||
Map(Attribute.ultraviolet_index, "Ultraviolet Index", None, None)
|
||||
Map(
|
||||
Attribute.ultraviolet_index,
|
||||
"Ultraviolet Index",
|
||||
None,
|
||||
None,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.voltage_measurement: [
|
||||
Map(Attribute.voltage, "Voltage Measurement", ELECTRIC_POTENTIAL_VOLT, None)
|
||||
Map(
|
||||
Attribute.voltage,
|
||||
"Voltage Measurement",
|
||||
ELECTRIC_POTENTIAL_VOLT,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
],
|
||||
Capability.washer_mode: [
|
||||
Map(Attribute.washer_mode, "Washer Mode", None, None, None)
|
||||
],
|
||||
Capability.washer_mode: [Map(Attribute.washer_mode, "Washer Mode", None, None)],
|
||||
Capability.washer_operating_state: [
|
||||
Map(Attribute.machine_state, "Washer Machine State", None, None),
|
||||
Map(Attribute.washer_job_state, "Washer Job State", None, None),
|
||||
Map(Attribute.machine_state, "Washer Machine State", None, None, None),
|
||||
Map(Attribute.washer_job_state, "Washer Job State", None, None, None),
|
||||
Map(
|
||||
Attribute.completion_time,
|
||||
"Washer Completion Time",
|
||||
None,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
None,
|
||||
),
|
||||
],
|
||||
}
|
||||
@@ -292,11 +408,34 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
sensors.extend(
|
||||
[
|
||||
SmartThingsSensor(
|
||||
device, m.attribute, m.name, m.default_unit, m.device_class
|
||||
device,
|
||||
m.attribute,
|
||||
m.name,
|
||||
m.default_unit,
|
||||
m.device_class,
|
||||
m.state_class,
|
||||
)
|
||||
for m in maps
|
||||
]
|
||||
)
|
||||
|
||||
if broker.any_assigned(device.device_id, "switch"):
|
||||
for capability in (Capability.energy_meter, Capability.power_meter):
|
||||
maps = CAPABILITY_TO_SENSORS[capability]
|
||||
sensors.extend(
|
||||
[
|
||||
SmartThingsSensor(
|
||||
device,
|
||||
m.attribute,
|
||||
m.name,
|
||||
m.default_unit,
|
||||
m.device_class,
|
||||
m.state_class,
|
||||
)
|
||||
for m in maps
|
||||
]
|
||||
)
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
@@ -311,14 +450,21 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
|
||||
"""Define a SmartThings Sensor."""
|
||||
|
||||
def __init__(
|
||||
self, device, attribute: str, name: str, default_unit: str, device_class: str
|
||||
):
|
||||
self,
|
||||
device: DeviceEntity,
|
||||
attribute: str,
|
||||
name: str,
|
||||
default_unit: str,
|
||||
device_class: str,
|
||||
state_class: str | None,
|
||||
) -> None:
|
||||
"""Init the class."""
|
||||
super().__init__(device)
|
||||
self._attribute = attribute
|
||||
self._name = name
|
||||
self._device_class = device_class
|
||||
self._default_unit = default_unit
|
||||
self._attr_state_class = state_class
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -346,6 +492,13 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
|
||||
unit = self._device.status.attributes[self._attribute].unit
|
||||
return UNITS.get(unit, unit) if unit else self._default_unit
|
||||
|
||||
@property
|
||||
def last_reset(self) -> datetime | None:
|
||||
"""Return the time when the sensor was last reset, if any."""
|
||||
if self._attribute == Attribute.energy:
|
||||
return utc_from_timestamp(0)
|
||||
return None
|
||||
|
||||
|
||||
class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity):
|
||||
"""Define a SmartThings Three Axis Sensor."""
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from pysmartthings import Attribute, Capability
|
||||
from pysmartthings import Capability
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
|
||||
@@ -48,16 +48,6 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchEntity):
|
||||
# the entity state ahead of receiving the confirming push updates
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def current_power_w(self):
|
||||
"""Return the current power usage in W."""
|
||||
return self._device.status.attributes[Attribute.power].value
|
||||
|
||||
@property
|
||||
def today_energy_kwh(self):
|
||||
"""Return the today total energy usage in kWh."""
|
||||
return self._device.status.attributes[Attribute.energy].value
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
|
||||
@@ -3,10 +3,16 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
|
||||
from homeassistant.const import ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
ENERGY_WATT_HOUR,
|
||||
PERCENTAGE,
|
||||
POWER_WATT,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .models import SolarEdgeSensor
|
||||
from .models import SolarEdgeSensorEntityDescription
|
||||
|
||||
DOMAIN = "solaredge"
|
||||
|
||||
@@ -29,7 +35,7 @@ SCAN_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
# Supported overview sensors
|
||||
SENSOR_TYPES = [
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="lifetime_energy",
|
||||
json_key="lifeTimeData",
|
||||
name="Lifetime energy",
|
||||
@@ -37,138 +43,143 @@ SENSOR_TYPES = [
|
||||
last_reset=dt_util.utc_from_timestamp(0),
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=ENERGY_WATT_HOUR,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="energy_this_year",
|
||||
json_key="lastYearData",
|
||||
name="Energy this year",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:solar-power",
|
||||
unit_of_measurement=ENERGY_WATT_HOUR,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="energy_this_month",
|
||||
json_key="lastMonthData",
|
||||
name="Energy this month",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:solar-power",
|
||||
unit_of_measurement=ENERGY_WATT_HOUR,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="energy_today",
|
||||
json_key="lastDayData",
|
||||
name="Energy today",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:solar-power",
|
||||
unit_of_measurement=ENERGY_WATT_HOUR,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="current_power",
|
||||
json_key="currentPower",
|
||||
name="Current Power",
|
||||
icon="mdi:solar-power",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=POWER_WATT,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="site_details",
|
||||
name="Site details",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="meters",
|
||||
json_key="meters",
|
||||
name="Meters",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="sensors",
|
||||
json_key="sensors",
|
||||
name="Sensors",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="gateways",
|
||||
json_key="gateways",
|
||||
name="Gateways",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="batteries",
|
||||
json_key="batteries",
|
||||
name="Batteries",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="inverters",
|
||||
json_key="inverters",
|
||||
name="Inverters",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="power_consumption",
|
||||
json_key="LOAD",
|
||||
name="Power Consumption",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="solar_power",
|
||||
json_key="PV",
|
||||
name="Solar Power",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:solar-power",
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="grid_power",
|
||||
json_key="GRID",
|
||||
name="Grid Power",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:power-plug",
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="storage_power",
|
||||
json_key="STORAGE",
|
||||
name="Storage Power",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:car-battery",
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="purchased_power",
|
||||
json_key="Purchased",
|
||||
name="Imported Power",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="production_power",
|
||||
json_key="Production",
|
||||
name="Production Power",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="consumption_power",
|
||||
json_key="Consumption",
|
||||
name="Consumption Power",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="selfconsumption_power",
|
||||
json_key="SelfConsumption",
|
||||
name="SelfConsumption Power",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="feedin_power",
|
||||
json_key="FeedIn",
|
||||
name="Exported Power",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SolarEdgeSensor(
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="storage_level",
|
||||
json_key="STORAGE",
|
||||
name="Storage Level",
|
||||
|
||||
@@ -2,20 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
|
||||
|
||||
@dataclass
|
||||
class SolarEdgeSensor:
|
||||
"""Represents an SolarEdge Sensor."""
|
||||
|
||||
key: str
|
||||
name: str
|
||||
class SolarEdgeSensorEntityDescription(SensorEntityDescription):
|
||||
"""Sensor entity description for SolarEdge."""
|
||||
|
||||
json_key: str | None = None
|
||||
device_class: str | None = None
|
||||
entity_registry_enabled_default: bool = True
|
||||
icon: str | None = None
|
||||
last_reset: datetime | None = None
|
||||
state_class: str | None = None
|
||||
unit_of_measurement: str | None = None
|
||||
|
||||
@@ -21,7 +21,7 @@ from .coordinator import (
|
||||
SolarEdgeOverviewDataService,
|
||||
SolarEdgePowerFlowDataService,
|
||||
)
|
||||
from .models import SolarEdgeSensor
|
||||
from .models import SolarEdgeSensorEntityDescription
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -68,7 +68,8 @@ class SolarEdgeSensorFactory:
|
||||
self.services: dict[
|
||||
str,
|
||||
tuple[
|
||||
type[SolarEdgeSensor | SolarEdgeOverviewSensor], SolarEdgeDataService
|
||||
type[SolarEdgeSensorEntity | SolarEdgeOverviewSensor],
|
||||
SolarEdgeDataService,
|
||||
],
|
||||
] = {"site_details": (SolarEdgeDetailsSensor, details)}
|
||||
|
||||
@@ -99,7 +100,9 @@ class SolarEdgeSensorFactory:
|
||||
):
|
||||
self.services[key] = (SolarEdgeEnergyDetailsSensor, energy)
|
||||
|
||||
def create_sensor(self, sensor_type: SolarEdgeSensor) -> SolarEdgeSensor:
|
||||
def create_sensor(
|
||||
self, sensor_type: SolarEdgeSensorEntityDescription
|
||||
) -> SolarEdgeSensorEntityDescription:
|
||||
"""Create and return a sensor based on the sensor_key."""
|
||||
sensor_class, service = self.services[sensor_type.key]
|
||||
|
||||
@@ -109,27 +112,21 @@ class SolarEdgeSensorFactory:
|
||||
class SolarEdgeSensorEntity(CoordinatorEntity, SensorEntity):
|
||||
"""Abstract class for a solaredge sensor."""
|
||||
|
||||
entity_description: SolarEdgeSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
platform_name: str,
|
||||
sensor_type: SolarEdgeSensor,
|
||||
description: SolarEdgeSensorEntityDescription,
|
||||
data_service: SolarEdgeDataService,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(data_service.coordinator)
|
||||
self.platform_name = platform_name
|
||||
self.sensor_type = sensor_type
|
||||
self.entity_description = description
|
||||
self.data_service = data_service
|
||||
|
||||
self._attr_device_class = sensor_type.device_class
|
||||
self._attr_entity_registry_enabled_default = (
|
||||
sensor_type.entity_registry_enabled_default
|
||||
)
|
||||
self._attr_icon = sensor_type.icon
|
||||
self._attr_last_reset = sensor_type.last_reset
|
||||
self._attr_name = f"{platform_name} ({sensor_type.name})"
|
||||
self._attr_state_class = sensor_type.state_class
|
||||
self._attr_unit_of_measurement = sensor_type.unit_of_measurement
|
||||
self._attr_name = f"{platform_name} ({description.name})"
|
||||
|
||||
|
||||
class SolarEdgeOverviewSensor(SolarEdgeSensorEntity):
|
||||
@@ -138,7 +135,7 @@ class SolarEdgeOverviewSensor(SolarEdgeSensorEntity):
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.data_service.data.get(self.sensor_type.json_key)
|
||||
return self.data_service.data.get(self.entity_description.json_key)
|
||||
|
||||
|
||||
class SolarEdgeDetailsSensor(SolarEdgeSensorEntity):
|
||||
@@ -161,12 +158,12 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity):
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
return self.data_service.attributes.get(self.sensor_type.json_key)
|
||||
return self.data_service.attributes.get(self.entity_description.json_key)
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.data_service.data.get(self.sensor_type.json_key)
|
||||
return self.data_service.data.get(self.entity_description.json_key)
|
||||
|
||||
|
||||
class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity):
|
||||
@@ -181,12 +178,12 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity):
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
return self.data_service.attributes.get(self.sensor_type.json_key)
|
||||
return self.data_service.attributes.get(self.entity_description.json_key)
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.data_service.data.get(self.sensor_type.json_key)
|
||||
return self.data_service.data.get(self.entity_description.json_key)
|
||||
|
||||
|
||||
class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity):
|
||||
@@ -197,23 +194,23 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
platform_name: str,
|
||||
sensor_type: SolarEdgeSensor,
|
||||
description: SolarEdgeSensorEntityDescription,
|
||||
data_service: SolarEdgeDataService,
|
||||
) -> None:
|
||||
"""Initialize the power flow sensor."""
|
||||
super().__init__(platform_name, sensor_type, data_service)
|
||||
super().__init__(platform_name, description, data_service)
|
||||
|
||||
self._attr_unit_of_measurement = data_service.unit
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
return self.data_service.attributes.get(self.sensor_type.json_key)
|
||||
return self.data_service.attributes.get(self.entity_description.json_key)
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.data_service.data.get(self.sensor_type.json_key)
|
||||
return self.data_service.data.get(self.entity_description.json_key)
|
||||
|
||||
|
||||
class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity):
|
||||
@@ -224,7 +221,7 @@ class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity):
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
attr = self.data_service.attributes.get(self.sensor_type.json_key)
|
||||
attr = self.data_service.attributes.get(self.entity_description.json_key)
|
||||
if attr and "soc" in attr:
|
||||
return attr["soc"]
|
||||
return None
|
||||
|
||||
@@ -256,9 +256,21 @@ class Scanner:
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start
|
||||
)
|
||||
await asyncio.gather(
|
||||
*(listener.async_start() for listener in self._ssdp_listeners)
|
||||
results = await asyncio.gather(
|
||||
*(listener.async_start() for listener in self._ssdp_listeners),
|
||||
return_exceptions=True,
|
||||
)
|
||||
failed_listeners = []
|
||||
for idx, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
_LOGGER.warning(
|
||||
"Failed to setup listener for %s: %s",
|
||||
self._ssdp_listeners[idx].source_ip,
|
||||
result,
|
||||
)
|
||||
failed_listeners.append(self._ssdp_listeners[idx])
|
||||
for listener in failed_listeners:
|
||||
self._ssdp_listeners.remove(listener)
|
||||
self._cancel_scan = async_track_time_interval(
|
||||
self.hass, self.async_scan, SCAN_INTERVAL
|
||||
)
|
||||
|
||||
@@ -175,7 +175,7 @@ class DeviceConnectivity(SurePetcareBinarySensor):
|
||||
"""Get the latest data and update the state."""
|
||||
surepy_entity = self._spc.states[self._id]
|
||||
state = surepy_entity.raw_data()["status"]
|
||||
self._attr_is_on = self._attr_available = bool(self.state)
|
||||
self._attr_is_on = self._attr_available = bool(state)
|
||||
if state:
|
||||
self._attr_extra_state_attributes = {
|
||||
"device_rssi": f'{state["signal"]["device_rssi"]:.2f}',
|
||||
|
||||
@@ -848,7 +848,7 @@ class BaseTelegramBotEntity:
|
||||
|
||||
if (
|
||||
msg_data["from"].get("id") not in self.allowed_chat_ids
|
||||
and msg_data["chat"].get("id") not in self.allowed_chat_ids
|
||||
and msg_data["message"]["chat"].get("id") not in self.allowed_chat_ids
|
||||
):
|
||||
# Neither from id nor chat id was in allowed_chat_ids,
|
||||
# origin is not allowed.
|
||||
|
||||
@@ -1,37 +1,57 @@
|
||||
"""Component to embed TP-Link smart home devices."""
|
||||
import logging
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import time
|
||||
|
||||
from pyHS100.smartdevice import SmartDevice, SmartDeviceException
|
||||
from pyHS100.smartplug import SmartPlug
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.sensor import ATTR_LAST_RESET
|
||||
from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.const import (
|
||||
ATTR_VOLTAGE,
|
||||
CONF_ALIAS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
from .common import (
|
||||
from .common import SmartDevices, async_discover_devices, get_static_devices
|
||||
from .const import (
|
||||
ATTR_CONFIG,
|
||||
ATTR_CURRENT_A,
|
||||
ATTR_TOTAL_ENERGY_KWH,
|
||||
CONF_DIMMER,
|
||||
CONF_DISCOVERY,
|
||||
CONF_EMETER_PARAMS,
|
||||
CONF_LIGHT,
|
||||
CONF_MODEL,
|
||||
CONF_STRIP,
|
||||
CONF_SW_VERSION,
|
||||
CONF_SWITCH,
|
||||
SmartDevices,
|
||||
async_discover_devices,
|
||||
get_static_devices,
|
||||
COORDINATORS,
|
||||
PLATFORMS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "tplink"
|
||||
|
||||
PLATFORMS = [CONF_LIGHT, CONF_SWITCH]
|
||||
|
||||
TPLINK_HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
@@ -82,8 +102,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
device_count = len(tplink_devices)
|
||||
|
||||
# These will contain the initialized devices
|
||||
lights = hass.data[DOMAIN][CONF_LIGHT] = []
|
||||
switches = hass.data[DOMAIN][CONF_SWITCH] = []
|
||||
hass.data[DOMAIN][CONF_LIGHT] = []
|
||||
hass.data[DOMAIN][CONF_SWITCH] = []
|
||||
lights: list[SmartDevice] = hass.data[DOMAIN][CONF_LIGHT]
|
||||
switches: list[SmartPlug] = hass.data[DOMAIN][CONF_SWITCH]
|
||||
|
||||
# Add static devices
|
||||
static_devices = SmartDevices()
|
||||
@@ -102,14 +124,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
lights.extend(discovered_devices.lights)
|
||||
switches.extend(discovered_devices.switches)
|
||||
|
||||
forward_setup = hass.config_entries.async_forward_entry_setup
|
||||
if lights:
|
||||
_LOGGER.debug(
|
||||
"Got %s lights: %s", len(lights), ", ".join(d.host for d in lights)
|
||||
)
|
||||
|
||||
hass.async_create_task(forward_setup(entry, "light"))
|
||||
|
||||
if switches:
|
||||
_LOGGER.debug(
|
||||
"Got %s switches: %s",
|
||||
@@ -117,7 +136,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
", ".join(d.host for d in switches),
|
||||
)
|
||||
|
||||
hass.async_create_task(forward_setup(entry, "switch"))
|
||||
# prepare DataUpdateCoordinators
|
||||
hass.data[DOMAIN][COORDINATORS] = {}
|
||||
for switch in switches:
|
||||
|
||||
try:
|
||||
await hass.async_add_executor_job(switch.get_sysinfo)
|
||||
except SmartDeviceException as ex:
|
||||
_LOGGER.debug(ex)
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
hass.data[DOMAIN][COORDINATORS][
|
||||
switch.mac
|
||||
] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
@@ -130,3 +165,70 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.data[DOMAIN].clear()
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""DataUpdateCoordinator to gather data for specific SmartPlug."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
smartplug: SmartPlug,
|
||||
) -> None:
|
||||
"""Initialize DataUpdateCoordinator to gather data for specific SmartPlug."""
|
||||
self.smartplug = smartplug
|
||||
|
||||
update_interval = timedelta(seconds=30)
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=smartplug.alias, update_interval=update_interval
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict:
|
||||
"""Fetch all device and sensor data from api."""
|
||||
try:
|
||||
info = self.smartplug.sys_info
|
||||
data = {
|
||||
CONF_HOST: self.smartplug.host,
|
||||
CONF_MAC: info["mac"],
|
||||
CONF_MODEL: info["model"],
|
||||
CONF_SW_VERSION: info["sw_ver"],
|
||||
}
|
||||
if self.smartplug.context is None:
|
||||
data[CONF_ALIAS] = info["alias"]
|
||||
data[CONF_DEVICE_ID] = info["mac"]
|
||||
data[CONF_STATE] = (
|
||||
self.smartplug.state == self.smartplug.SWITCH_STATE_ON
|
||||
)
|
||||
else:
|
||||
plug_from_context = next(
|
||||
c
|
||||
for c in self.smartplug.sys_info["children"]
|
||||
if c["id"] == self.smartplug.context
|
||||
)
|
||||
data[CONF_ALIAS] = plug_from_context["alias"]
|
||||
data[CONF_DEVICE_ID] = self.smartplug.context
|
||||
data[CONF_STATE] = plug_from_context["state"] == 1
|
||||
if self.smartplug.has_emeter:
|
||||
emeter_readings = self.smartplug.get_emeter_realtime()
|
||||
data[CONF_EMETER_PARAMS] = {
|
||||
ATTR_CURRENT_POWER_W: round(float(emeter_readings["power"]), 2),
|
||||
ATTR_TOTAL_ENERGY_KWH: round(float(emeter_readings["total"]), 3),
|
||||
ATTR_VOLTAGE: round(float(emeter_readings["voltage"]), 1),
|
||||
ATTR_CURRENT_A: round(float(emeter_readings["current"]), 2),
|
||||
ATTR_LAST_RESET: {ATTR_TOTAL_ENERGY_KWH: utc_from_timestamp(0)},
|
||||
}
|
||||
emeter_statics = self.smartplug.get_emeter_daily()
|
||||
data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][
|
||||
ATTR_TODAY_ENERGY_KWH
|
||||
] = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if emeter_statics.get(int(time.strftime("%e"))):
|
||||
data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = round(
|
||||
float(emeter_statics[int(time.strftime("%e"))]), 3
|
||||
)
|
||||
else:
|
||||
# today's consumption not available, when device was off all the day
|
||||
data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = 0.0
|
||||
except SmartDeviceException as ex:
|
||||
raise UpdateFailed(ex) from ex
|
||||
|
||||
return data
|
||||
|
||||
@@ -14,21 +14,20 @@ from pyHS100 import (
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN as TPLINK_DOMAIN
|
||||
from .const import (
|
||||
CONF_DIMMER,
|
||||
CONF_LIGHT,
|
||||
CONF_STRIP,
|
||||
CONF_SWITCH,
|
||||
DOMAIN as TPLINK_DOMAIN,
|
||||
MAX_DISCOVERY_RETRIES,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ATTR_CONFIG = "config"
|
||||
CONF_DIMMER = "dimmer"
|
||||
CONF_DISCOVERY = "discovery"
|
||||
CONF_LIGHT = "light"
|
||||
CONF_STRIP = "strip"
|
||||
CONF_SWITCH = "switch"
|
||||
MAX_DISCOVERY_RETRIES = 4
|
||||
|
||||
|
||||
class SmartDevices:
|
||||
"""Hold different kinds of devices."""
|
||||
|
||||
@@ -98,7 +97,7 @@ async def async_discover_devices(
|
||||
else:
|
||||
_LOGGER.error("Unknown smart device type: %s", type(dev))
|
||||
|
||||
devices = {}
|
||||
devices: dict[str, SmartDevice] = {}
|
||||
for attempt in range(1, MAX_DISCOVERY_RETRIES + 1):
|
||||
_LOGGER.debug(
|
||||
"Discovering tplink devices, attempt %s of %s",
|
||||
@@ -159,16 +158,18 @@ def get_static_devices(config_data) -> SmartDevices:
|
||||
|
||||
def add_available_devices(
|
||||
hass: HomeAssistant, device_type: str, device_class: Callable
|
||||
) -> list:
|
||||
) -> list[Entity]:
|
||||
"""Get sysinfo for all devices."""
|
||||
|
||||
devices = hass.data[TPLINK_DOMAIN][device_type]
|
||||
devices: list[SmartDevice] = hass.data[TPLINK_DOMAIN][device_type]
|
||||
|
||||
if f"{device_type}_remaining" in hass.data[TPLINK_DOMAIN]:
|
||||
devices = hass.data[TPLINK_DOMAIN][f"{device_type}_remaining"]
|
||||
devices: list[SmartDevice] = hass.data[TPLINK_DOMAIN][
|
||||
f"{device_type}_remaining"
|
||||
]
|
||||
|
||||
entities_ready = []
|
||||
devices_unavailable = []
|
||||
entities_ready: list[Entity] = []
|
||||
devices_unavailable: list[SmartDevice] = []
|
||||
for device in devices:
|
||||
try:
|
||||
device.get_sysinfo()
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
"""Const for TP-Link."""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
DOMAIN = "tplink"
|
||||
COORDINATORS = "coordinators"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8)
|
||||
MAX_DISCOVERY_RETRIES = 4
|
||||
|
||||
ATTR_CONFIG = "config"
|
||||
ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh"
|
||||
ATTR_CURRENT_A = "current_a"
|
||||
|
||||
CONF_MODEL = "model"
|
||||
CONF_SW_VERSION = "sw_ver"
|
||||
CONF_EMETER_PARAMS = "emeter_params"
|
||||
CONF_DIMMER = "dimmer"
|
||||
CONF_DISCOVERY = "discovery"
|
||||
CONF_LIGHT = "light"
|
||||
CONF_STRIP = "strip"
|
||||
CONF_SWITCH = "switch"
|
||||
CONF_SENSOR = "sensor"
|
||||
|
||||
PLATFORMS = [CONF_LIGHT, CONF_SENSOR, CONF_SWITCH]
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Support for TPLink HS100/HS110/HS200 smart switch energy sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Final
|
||||
|
||||
from pyHS100 import SmartPlug
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_LAST_RESET,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.components.tplink import SmartPlugDataUpdateCoordinator
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_VOLTAGE,
|
||||
CONF_ALIAS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_MAC,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ELECTRIC_CURRENT_AMPERE,
|
||||
ELECTRIC_POTENTIAL_VOLT,
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
POWER_WATT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_EMETER_PARAMS,
|
||||
CONF_MODEL,
|
||||
CONF_SW_VERSION,
|
||||
CONF_SWITCH,
|
||||
COORDINATORS,
|
||||
DOMAIN as TPLINK_DOMAIN,
|
||||
)
|
||||
|
||||
ATTR_CURRENT_A = "current_a"
|
||||
ATTR_CURRENT_POWER_W = "current_power_w"
|
||||
ATTR_TODAY_ENERGY_KWH = "today_energy_kwh"
|
||||
ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh"
|
||||
|
||||
ENERGY_SENSORS: Final[list[SensorEntityDescription]] = [
|
||||
SensorEntityDescription(
|
||||
key=ATTR_CURRENT_POWER_W,
|
||||
unit_of_measurement=POWER_WATT,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
name="Current Consumption",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_TOTAL_ENERGY_KWH,
|
||||
unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
name="Total Consumption",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_TODAY_ENERGY_KWH,
|
||||
unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
name="Today's Consumption",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_VOLTAGE,
|
||||
unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
name="Voltage",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_CURRENT_A,
|
||||
unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
name="Current",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switches."""
|
||||
entities: list[SmartPlugSensor] = []
|
||||
coordinators: list[SmartPlugDataUpdateCoordinator] = hass.data[TPLINK_DOMAIN][
|
||||
COORDINATORS
|
||||
]
|
||||
switches: list[SmartPlug] = hass.data[TPLINK_DOMAIN][CONF_SWITCH]
|
||||
for switch in switches:
|
||||
coordinator: SmartPlugDataUpdateCoordinator = coordinators[switch.mac]
|
||||
if not switch.has_emeter and coordinator.data.get(CONF_EMETER_PARAMS) is None:
|
||||
continue
|
||||
for description in ENERGY_SENSORS:
|
||||
if coordinator.data[CONF_EMETER_PARAMS].get(description.key) is not None:
|
||||
entities.append(SmartPlugSensor(switch, coordinator, description))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SmartPlugSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Representation of a TPLink Smart Plug energy sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
smartplug: SmartPlug,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(coordinator)
|
||||
self.smartplug = smartplug
|
||||
self.entity_description = description
|
||||
self._attr_name = f"{coordinator.data[CONF_ALIAS]} {description.name}"
|
||||
self._attr_last_reset = coordinator.data[CONF_EMETER_PARAMS][
|
||||
ATTR_LAST_RESET
|
||||
].get(description.key)
|
||||
|
||||
@property
|
||||
def data(self) -> dict[str, Any]:
|
||||
"""Return data from DataUpdateCoordinator."""
|
||||
return self.coordinator.data
|
||||
|
||||
@property
|
||||
def state(self) -> float | None:
|
||||
"""Return the sensors state."""
|
||||
return self.data[CONF_EMETER_PARAMS][self.entity_description.key]
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str | None:
|
||||
"""Return a unique ID."""
|
||||
return f"{self.data[CONF_DEVICE_ID]}_{self.entity_description.key}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return information about the device."""
|
||||
return {
|
||||
"name": self.data[CONF_ALIAS],
|
||||
"model": self.data[CONF_MODEL],
|
||||
"manufacturer": "TP-Link",
|
||||
"connections": {(dr.CONNECTION_NETWORK_MAC, self.data[CONF_MAC])},
|
||||
"sw_version": self.data[CONF_SW_VERSION],
|
||||
}
|
||||
@@ -1,40 +1,30 @@
|
||||
"""Support for TPLink HS100/HS110/HS200 smart switch."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from pyHS100 import SmartDeviceException, SmartPlug
|
||||
from pyHS100 import SmartPlug
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
ATTR_CURRENT_POWER_W,
|
||||
ATTR_TODAY_ENERGY_KWH,
|
||||
SwitchEntity,
|
||||
)
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.components.tplink import SmartPlugDataUpdateCoordinator
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_VOLTAGE
|
||||
from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_MAC, CONF_STATE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from . import CONF_SWITCH, DOMAIN as TPLINK_DOMAIN
|
||||
from .common import add_available_devices
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh"
|
||||
ATTR_CURRENT_A = "current_a"
|
||||
|
||||
MAX_ATTEMPTS = 300
|
||||
SLEEP_TIME = 2
|
||||
from .const import (
|
||||
CONF_MODEL,
|
||||
CONF_SW_VERSION,
|
||||
CONF_SWITCH,
|
||||
COORDINATORS,
|
||||
DOMAIN as TPLINK_DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -43,164 +33,65 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switches."""
|
||||
entities = await hass.async_add_executor_job(
|
||||
add_available_devices, hass, CONF_SWITCH, SmartPlugSwitch
|
||||
)
|
||||
entities: list[SmartPlugSwitch] = []
|
||||
coordinators: list[SmartPlugDataUpdateCoordinator] = hass.data[TPLINK_DOMAIN][
|
||||
COORDINATORS
|
||||
]
|
||||
switches: list[SmartPlug] = hass.data[TPLINK_DOMAIN][CONF_SWITCH]
|
||||
for switch in switches:
|
||||
coordinator = coordinators[switch.mac]
|
||||
entities.append(SmartPlugSwitch(switch, coordinator))
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
if hass.data[TPLINK_DOMAIN][f"{CONF_SWITCH}_remaining"]:
|
||||
raise PlatformNotReady
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SmartPlugSwitch(SwitchEntity):
|
||||
class SmartPlugSwitch(CoordinatorEntity, SwitchEntity):
|
||||
"""Representation of a TPLink Smart Plug switch."""
|
||||
|
||||
def __init__(self, smartplug: SmartPlug) -> None:
|
||||
def __init__(
|
||||
self, smartplug: SmartPlug, coordinator: DataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(coordinator)
|
||||
self.smartplug = smartplug
|
||||
self._sysinfo = None
|
||||
self._state = None
|
||||
self._is_available = False
|
||||
# Set up emeter cache
|
||||
self._emeter_params = {}
|
||||
|
||||
self._mac = None
|
||||
self._alias = None
|
||||
self._model = None
|
||||
self._device_id = None
|
||||
self._host = None
|
||||
@property
|
||||
def data(self) -> dict[str, Any]:
|
||||
"""Return data from DataUpdateCoordinator."""
|
||||
return self.coordinator.data
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str | None:
|
||||
"""Return a unique ID."""
|
||||
return self._device_id
|
||||
return self.data[CONF_DEVICE_ID]
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
"""Return the name of the Smart Plug."""
|
||||
return self._alias
|
||||
return self.data[CONF_ALIAS]
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return information about the device."""
|
||||
return {
|
||||
"name": self._alias,
|
||||
"model": self._model,
|
||||
"name": self.data[CONF_ALIAS],
|
||||
"model": self.data[CONF_MODEL],
|
||||
"manufacturer": "TP-Link",
|
||||
"connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)},
|
||||
"sw_version": self._sysinfo["sw_ver"],
|
||||
"connections": {(dr.CONNECTION_NETWORK_MAC, self.data[CONF_MAC])},
|
||||
"sw_version": self.data[CONF_SW_VERSION],
|
||||
}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if switch is available."""
|
||||
return self._is_available
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if switch is on."""
|
||||
return self._state
|
||||
return self.data[CONF_STATE]
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
self.smartplug.turn_on()
|
||||
await self.hass.async_add_executor_job(self.smartplug.turn_on)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
self.smartplug.turn_off()
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return the state attributes of the device."""
|
||||
return self._emeter_params
|
||||
|
||||
@property
|
||||
def _plug_from_context(self) -> Any:
|
||||
"""Return the plug from the context."""
|
||||
children = self.smartplug.sys_info["children"]
|
||||
return next(c for c in children if c["id"] == self.smartplug.context)
|
||||
|
||||
def update_state(self) -> None:
|
||||
"""Update the TP-Link switch's state."""
|
||||
if self.smartplug.context is None:
|
||||
self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON
|
||||
else:
|
||||
self._state = self._plug_from_context["state"] == 1
|
||||
|
||||
def attempt_update(self, update_attempt: int) -> bool:
|
||||
"""Attempt to get details from the TP-Link switch."""
|
||||
try:
|
||||
if not self._sysinfo:
|
||||
self._sysinfo = self.smartplug.sys_info
|
||||
self._mac = self._sysinfo["mac"]
|
||||
self._model = self._sysinfo["model"]
|
||||
self._host = self.smartplug.host
|
||||
if self.smartplug.context is None:
|
||||
self._alias = self._sysinfo["alias"]
|
||||
self._device_id = self._mac
|
||||
else:
|
||||
self._alias = self._plug_from_context["alias"]
|
||||
self._device_id = self.smartplug.context
|
||||
|
||||
self.update_state()
|
||||
|
||||
if self.smartplug.has_emeter:
|
||||
emeter_readings = self.smartplug.get_emeter_realtime()
|
||||
|
||||
self._emeter_params[ATTR_CURRENT_POWER_W] = round(
|
||||
float(emeter_readings["power"]), 2
|
||||
)
|
||||
self._emeter_params[ATTR_TOTAL_ENERGY_KWH] = round(
|
||||
float(emeter_readings["total"]), 3
|
||||
)
|
||||
self._emeter_params[ATTR_VOLTAGE] = round(
|
||||
float(emeter_readings["voltage"]), 1
|
||||
)
|
||||
self._emeter_params[ATTR_CURRENT_A] = round(
|
||||
float(emeter_readings["current"]), 2
|
||||
)
|
||||
|
||||
emeter_statics = self.smartplug.get_emeter_daily()
|
||||
with suppress(KeyError): # Device returned no daily history
|
||||
self._emeter_params[ATTR_TODAY_ENERGY_KWH] = round(
|
||||
float(emeter_statics[int(time.strftime("%e"))]), 3
|
||||
)
|
||||
return True
|
||||
except (SmartDeviceException, OSError) as ex:
|
||||
if update_attempt == 0:
|
||||
_LOGGER.debug(
|
||||
"Retrying in %s seconds for %s|%s due to: %s",
|
||||
SLEEP_TIME,
|
||||
self._host,
|
||||
self._alias,
|
||||
ex,
|
||||
)
|
||||
return False
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the TP-Link switch's state."""
|
||||
for update_attempt in range(MAX_ATTEMPTS):
|
||||
is_ready = await self.hass.async_add_executor_job(
|
||||
self.attempt_update, update_attempt
|
||||
)
|
||||
|
||||
if is_ready:
|
||||
self._is_available = True
|
||||
if update_attempt > 0:
|
||||
_LOGGER.debug(
|
||||
"Device %s|%s responded after %s attempts",
|
||||
self._host,
|
||||
self._alias,
|
||||
update_attempt,
|
||||
)
|
||||
break
|
||||
await asyncio.sleep(SLEEP_TIME)
|
||||
|
||||
else:
|
||||
if self._is_available:
|
||||
_LOGGER.warning(
|
||||
"Could not read state for %s|%s", self.smartplug.host, self._alias
|
||||
)
|
||||
self._is_available = False
|
||||
await self.hass.async_add_executor_job(self.smartplug.turn_off)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Belkin WeMo",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/wemo",
|
||||
"requirements": ["pywemo==0.6.5"],
|
||||
"requirements": ["pywemo==0.6.6"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Belkin International Inc."
|
||||
|
||||
@@ -21,7 +21,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
KEY_COORDINATOR,
|
||||
KEY_DEVICE,
|
||||
KEY_MIGRATE_ENTITY_NAME,
|
||||
MODELS_AIR_MONITOR,
|
||||
MODELS_FAN,
|
||||
MODELS_HUMIDIFIER,
|
||||
@@ -112,12 +111,13 @@ async def async_create_miio_device_and_coordinator(
|
||||
else:
|
||||
device = AirHumidifier(host, token, model=model)
|
||||
|
||||
# Removing fan platform entity for humidifiers and cache the name and entity name for migration
|
||||
# Removing fan platform entity for humidifiers and migrate the name to the config entry for migration
|
||||
entity_registry = er.async_get(hass)
|
||||
entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id)
|
||||
if entity_id:
|
||||
# This check is entities that have a platform migration only and should be removed in the future
|
||||
migrate_entity_name = entity_registry.async_get(entity_id).name
|
||||
hass.config_entries.async_update_entry(entry, title=migrate_entity_name)
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
async def async_update_data():
|
||||
@@ -142,8 +142,6 @@ async def async_create_miio_device_and_coordinator(
|
||||
KEY_DEVICE: device,
|
||||
KEY_COORDINATOR: coordinator,
|
||||
}
|
||||
if migrate_entity_name:
|
||||
hass.data[DOMAIN][entry.entry_id][KEY_MIGRATE_ENTITY_NAME] = migrate_entity_name
|
||||
|
||||
# Trigger first data fetch
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -18,7 +18,6 @@ CONF_CLOUD_SUBDEVICES = "cloud_subdevices"
|
||||
# Keys
|
||||
KEY_COORDINATOR = "coordinator"
|
||||
KEY_DEVICE = "device"
|
||||
KEY_MIGRATE_ENTITY_NAME = "migrate_entity_name"
|
||||
|
||||
# Attributes
|
||||
ATTR_AVAILABLE = "available"
|
||||
|
||||
@@ -24,7 +24,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
KEY_COORDINATOR,
|
||||
KEY_DEVICE,
|
||||
KEY_MIGRATE_ENTITY_NAME,
|
||||
MODEL_AIRHUMIDIFIER_CA1,
|
||||
MODEL_AIRHUMIDIFIER_CA4,
|
||||
MODEL_AIRHUMIDIFIER_CB1,
|
||||
@@ -52,10 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
model = config_entry.data[CONF_MODEL]
|
||||
unique_id = config_entry.unique_id
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
|
||||
if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]:
|
||||
name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME]
|
||||
else:
|
||||
name = config_entry.title
|
||||
name = config_entry.title
|
||||
|
||||
if model in MODELS_HUMIDIFIER_MIOT:
|
||||
air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
|
||||
|
||||
@@ -13,7 +13,6 @@ from .const import (
|
||||
FEATURE_SET_MOTOR_SPEED,
|
||||
KEY_COORDINATOR,
|
||||
KEY_DEVICE,
|
||||
KEY_MIGRATE_ENTITY_NAME,
|
||||
MODEL_AIRHUMIDIFIER_CA4,
|
||||
)
|
||||
from .device import XiaomiCoordinatedMiioEntity
|
||||
@@ -58,10 +57,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
model = config_entry.data[CONF_MODEL]
|
||||
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
|
||||
if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]:
|
||||
name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME]
|
||||
else:
|
||||
name = config_entry.title
|
||||
|
||||
if model not in [MODEL_AIRHUMIDIFIER_CA4]:
|
||||
return
|
||||
@@ -69,7 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
for number in NUMBER_TYPES.values():
|
||||
entities.append(
|
||||
XiaomiAirHumidifierNumber(
|
||||
f"{name} {number.name}",
|
||||
f"{config_entry.title} {number.name}",
|
||||
device,
|
||||
config_entry,
|
||||
f"{number.short_name}_{config_entry.unique_id}",
|
||||
|
||||
@@ -16,7 +16,6 @@ from .const import (
|
||||
FEATURE_SET_LED_BRIGHTNESS,
|
||||
KEY_COORDINATOR,
|
||||
KEY_DEVICE,
|
||||
KEY_MIGRATE_ENTITY_NAME,
|
||||
MODEL_AIRHUMIDIFIER_CA1,
|
||||
MODEL_AIRHUMIDIFIER_CA4,
|
||||
MODEL_AIRHUMIDIFIER_CB1,
|
||||
@@ -67,10 +66,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
model = config_entry.data[CONF_MODEL]
|
||||
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
|
||||
if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]:
|
||||
name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME]
|
||||
else:
|
||||
name = config_entry.title
|
||||
|
||||
if model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]:
|
||||
entity_class = XiaomiAirHumidifierSelector
|
||||
@@ -84,7 +79,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
for selector in SELECTOR_TYPES.values():
|
||||
entities.append(
|
||||
entity_class(
|
||||
f"{name} {selector.name}",
|
||||
f"{config_entry.title} {selector.name}",
|
||||
device,
|
||||
config_entry,
|
||||
f"{selector.short_name}_{config_entry.unique_id}",
|
||||
|
||||
@@ -45,7 +45,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
KEY_COORDINATOR,
|
||||
KEY_DEVICE,
|
||||
KEY_MIGRATE_ENTITY_NAME,
|
||||
MODELS_HUMIDIFIER_MIOT,
|
||||
)
|
||||
from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity
|
||||
@@ -190,11 +189,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
model = config_entry.data[CONF_MODEL]
|
||||
device = None
|
||||
sensors = []
|
||||
if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]:
|
||||
name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME]
|
||||
else:
|
||||
name = config_entry.title
|
||||
|
||||
if model in MODELS_HUMIDIFIER_MIOT:
|
||||
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
|
||||
@@ -205,6 +199,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
sensors = HUMIDIFIER_SENSORS
|
||||
else:
|
||||
unique_id = config_entry.unique_id
|
||||
name = config_entry.title
|
||||
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
|
||||
|
||||
device = AirQualityMonitor(host, token)
|
||||
@@ -214,7 +209,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
for sensor in sensors:
|
||||
entities.append(
|
||||
XiaomiGenericSensor(
|
||||
f"{name} {sensor.replace('_', ' ').title()}",
|
||||
f"{config_entry.title} {sensor.replace('_', ' ').title()}",
|
||||
device,
|
||||
config_entry,
|
||||
f"{sensor}_{config_entry.unique_id}",
|
||||
|
||||
@@ -41,7 +41,6 @@ from .const import (
|
||||
FEATURE_SET_DRY,
|
||||
KEY_COORDINATOR,
|
||||
KEY_DEVICE,
|
||||
KEY_MIGRATE_ENTITY_NAME,
|
||||
MODEL_AIRHUMIDIFIER_CA1,
|
||||
MODEL_AIRHUMIDIFIER_CA4,
|
||||
MODEL_AIRHUMIDIFIER_CB1,
|
||||
@@ -219,13 +218,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the switch from a config entry."""
|
||||
if (
|
||||
config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY
|
||||
or config_entry.data[CONF_MODEL] == "lumi.acpartner.v3"
|
||||
):
|
||||
await async_setup_other_entry(hass, config_entry, async_add_entities)
|
||||
else:
|
||||
if config_entry.data[CONF_MODEL] in MODELS_HUMIDIFIER:
|
||||
await async_setup_coordinated_entry(hass, config_entry, async_add_entities)
|
||||
else:
|
||||
await async_setup_other_entry(hass, config_entry, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_coordinated_entry(hass, config_entry, async_add_entities):
|
||||
@@ -235,10 +231,6 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities):
|
||||
unique_id = config_entry.unique_id
|
||||
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
|
||||
if KEY_MIGRATE_ENTITY_NAME in hass.data[DOMAIN][config_entry.entry_id]:
|
||||
name = hass.data[DOMAIN][config_entry.entry_id][KEY_MIGRATE_ENTITY_NAME]
|
||||
else:
|
||||
name = config_entry.title
|
||||
|
||||
if DATA_KEY not in hass.data:
|
||||
hass.data[DATA_KEY] = {}
|
||||
@@ -256,7 +248,7 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities):
|
||||
if feature & device_features:
|
||||
entities.append(
|
||||
XiaomiGenericCoordinatedSwitch(
|
||||
f"{name} {switch.name}",
|
||||
f"{config_entry.title} {switch.name}",
|
||||
device,
|
||||
config_entry,
|
||||
f"{switch.short_name}_{unique_id}",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"zha-quirks==0.0.59",
|
||||
"zigpy-cc==0.5.2",
|
||||
"zigpy-deconz==0.12.0",
|
||||
"zigpy==0.36.0",
|
||||
"zigpy==0.36.1",
|
||||
"zigpy-xbee==0.13.0",
|
||||
"zigpy-zigate==0.7.3",
|
||||
"zigpy-znp==0.5.2"
|
||||
|
||||
@@ -277,12 +277,6 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
|
||||
if self.info.primary_value.command_class == CommandClass.BATTERY
|
||||
else None
|
||||
)
|
||||
# Legacy binary sensors are phased out (replaced by notification sensors)
|
||||
# Disable by default to not confuse users
|
||||
self._attr_entity_registry_enabled_default = bool(
|
||||
self.info.primary_value.command_class != CommandClass.SENSOR_BINARY
|
||||
or self.info.node.device_class.generic.key == 0x20
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
|
||||
@@ -67,6 +67,8 @@ class ZwaveDiscoveryInfo:
|
||||
platform_data: dict[str, Any] | None = None
|
||||
# additional values that need to be watched by entity
|
||||
additional_value_ids_to_watch: set[str] | None = None
|
||||
# bool to specify whether entity should be enabled by default
|
||||
entity_registry_enabled_default: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -135,6 +137,8 @@ class ZWaveDiscoverySchema:
|
||||
allow_multi: bool = False
|
||||
# [optional] bool to specify whether state is assumed and events should be fired on value update
|
||||
assumed_state: bool = False
|
||||
# [optional] bool to specify whether entity should be enabled by default
|
||||
entity_registry_enabled_default: bool = True
|
||||
|
||||
|
||||
def get_config_parameter_discovery_schema(
|
||||
@@ -161,6 +165,7 @@ def get_config_parameter_discovery_schema(
|
||||
property_key_name=property_key_name,
|
||||
type={"number"},
|
||||
),
|
||||
entity_registry_enabled_default=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -428,12 +433,33 @@ DISCOVERY_SCHEMAS = [
|
||||
],
|
||||
),
|
||||
# binary sensors
|
||||
# When CC is Sensor Binary and device class generic is Binary Sensor, entity should
|
||||
# be enabled by default
|
||||
ZWaveDiscoverySchema(
|
||||
platform="binary_sensor",
|
||||
hint="boolean",
|
||||
device_class_generic={"Binary Sensor"},
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.SENSOR_BINARY},
|
||||
type={"boolean"},
|
||||
),
|
||||
),
|
||||
# Legacy binary sensors are phased out (replaced by notification sensors)
|
||||
# Disable by default to not confuse users
|
||||
ZWaveDiscoverySchema(
|
||||
platform="binary_sensor",
|
||||
hint="boolean",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.SENSOR_BINARY},
|
||||
type={"boolean"},
|
||||
),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
ZWaveDiscoverySchema(
|
||||
platform="binary_sensor",
|
||||
hint="boolean",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={
|
||||
CommandClass.SENSOR_BINARY,
|
||||
CommandClass.BATTERY,
|
||||
CommandClass.SENSOR_ALARM,
|
||||
},
|
||||
@@ -456,13 +482,19 @@ DISCOVERY_SCHEMAS = [
|
||||
platform="sensor",
|
||||
hint="string_sensor",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={
|
||||
CommandClass.SENSOR_ALARM,
|
||||
CommandClass.INDICATOR,
|
||||
},
|
||||
command_class={CommandClass.SENSOR_ALARM},
|
||||
type={"string"},
|
||||
),
|
||||
),
|
||||
ZWaveDiscoverySchema(
|
||||
platform="sensor",
|
||||
hint="string_sensor",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.INDICATOR},
|
||||
type={"string"},
|
||||
),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# generic numeric sensors
|
||||
ZWaveDiscoverySchema(
|
||||
platform="sensor",
|
||||
@@ -471,16 +503,24 @@ DISCOVERY_SCHEMAS = [
|
||||
command_class={
|
||||
CommandClass.SENSOR_MULTILEVEL,
|
||||
CommandClass.SENSOR_ALARM,
|
||||
CommandClass.INDICATOR,
|
||||
CommandClass.BATTERY,
|
||||
},
|
||||
type={"number"},
|
||||
),
|
||||
),
|
||||
# numeric sensors for Meter CC
|
||||
ZWaveDiscoverySchema(
|
||||
platform="sensor",
|
||||
hint="numeric_sensor",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.INDICATOR},
|
||||
type={"number"},
|
||||
),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# Meter sensors for Meter CC
|
||||
ZWaveDiscoverySchema(
|
||||
platform="sensor",
|
||||
hint="meter",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={
|
||||
CommandClass.METER,
|
||||
@@ -500,6 +540,7 @@ DISCOVERY_SCHEMAS = [
|
||||
type={"number"},
|
||||
),
|
||||
allow_multi=True,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# sensor for basic CC
|
||||
ZWaveDiscoverySchema(
|
||||
@@ -512,6 +553,7 @@ DISCOVERY_SCHEMAS = [
|
||||
type={"number"},
|
||||
property={"currentValue"},
|
||||
),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# binary switches
|
||||
ZWaveDiscoverySchema(
|
||||
@@ -697,6 +739,7 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None
|
||||
platform_data_template=schema.data_template,
|
||||
platform_data=resolved_data,
|
||||
additional_value_ids_to_watch=additional_value_ids_to_watch,
|
||||
entity_registry_enabled_default=schema.entity_registry_enabled_default,
|
||||
)
|
||||
|
||||
if not schema.allow_multi:
|
||||
|
||||
@@ -46,6 +46,9 @@ class ZWaveBaseEntity(Entity):
|
||||
self._attr_unique_id = get_unique_id(
|
||||
self.client.driver.controller.home_id, self.info.primary_value.value_id
|
||||
)
|
||||
self._attr_entity_registry_enabled_default = (
|
||||
self.info.entity_registry_enabled_default
|
||||
)
|
||||
self._attr_assumed_state = self.info.assumed_state
|
||||
# device is precreated in main handler
|
||||
self._attr_device_info = {
|
||||
|
||||
@@ -11,6 +11,7 @@ from zwave_js_server.model.node import Node as ZwaveNode
|
||||
from zwave_js_server.model.value import ConfigurationValue
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_LAST_RESET,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
@@ -21,15 +22,22 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.util import dt
|
||||
|
||||
from .const import ATTR_METER_TYPE, ATTR_VALUE, DATA_CLIENT, DOMAIN, SERVICE_RESET_METER
|
||||
from .discovery import ZwaveDiscoveryInfo
|
||||
@@ -60,6 +68,8 @@ async def async_setup_entry(
|
||||
entities.append(ZWaveListSensor(config_entry, client, info))
|
||||
elif info.platform_hint == "config_parameter":
|
||||
entities.append(ZWaveConfigParameterSensor(config_entry, client, info))
|
||||
elif info.platform_hint == "meter":
|
||||
entities.append(ZWaveMeterSensor(config_entry, client, info))
|
||||
else:
|
||||
LOGGER.warning(
|
||||
"Sensor not implemented for %s/%s",
|
||||
@@ -118,15 +128,6 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity):
|
||||
self._attr_name = self.generate_name(include_value_name=True)
|
||||
self._attr_device_class = self._get_device_class()
|
||||
self._attr_state_class = self._get_state_class()
|
||||
self._attr_entity_registry_enabled_default = True
|
||||
# We hide some of the more advanced sensors by default to not overwhelm users
|
||||
if self.info.primary_value.command_class in [
|
||||
CommandClass.BASIC,
|
||||
CommandClass.CONFIGURATION,
|
||||
CommandClass.INDICATOR,
|
||||
CommandClass.NOTIFICATION,
|
||||
]:
|
||||
self._attr_entity_registry_enabled_default = False
|
||||
|
||||
def _get_device_class(self) -> str | None:
|
||||
"""
|
||||
@@ -137,18 +138,20 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity):
|
||||
"""
|
||||
if self.info.primary_value.command_class == CommandClass.BATTERY:
|
||||
return DEVICE_CLASS_BATTERY
|
||||
if self.info.primary_value.command_class == CommandClass.METER:
|
||||
if self.info.primary_value.metadata.unit == "kWh":
|
||||
return DEVICE_CLASS_ENERGY
|
||||
return DEVICE_CLASS_POWER
|
||||
if isinstance(self.info.primary_value.property_, str):
|
||||
property_lower = self.info.primary_value.property_.lower()
|
||||
if "humidity" in property_lower:
|
||||
return DEVICE_CLASS_HUMIDITY
|
||||
if "temperature" in property_lower:
|
||||
return DEVICE_CLASS_TEMPERATURE
|
||||
if self.info.primary_value.metadata.unit == "A":
|
||||
return DEVICE_CLASS_CURRENT
|
||||
if self.info.primary_value.metadata.unit == "W":
|
||||
return DEVICE_CLASS_POWER
|
||||
if self.info.primary_value.metadata.unit == "kWh":
|
||||
return DEVICE_CLASS_ENERGY
|
||||
if self.info.primary_value.metadata.unit == "V":
|
||||
return DEVICE_CLASS_VOLTAGE
|
||||
if self.info.primary_value.metadata.unit == "Lux":
|
||||
return DEVICE_CLASS_ILLUMINANCE
|
||||
return None
|
||||
@@ -230,14 +233,68 @@ class ZWaveNumericSensor(ZwaveSensorBase):
|
||||
|
||||
return str(self.info.primary_value.metadata.unit)
|
||||
|
||||
|
||||
class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity):
|
||||
"""Representation of a Z-Wave Meter CC sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
client: ZwaveClient,
|
||||
info: ZwaveDiscoveryInfo,
|
||||
) -> None:
|
||||
"""Initialize a ZWaveNumericSensor entity."""
|
||||
super().__init__(config_entry, client, info)
|
||||
|
||||
# Entity class attributes
|
||||
self._attr_state_class = STATE_CLASS_MEASUREMENT
|
||||
self._attr_last_reset = dt.utc_from_timestamp(0)
|
||||
|
||||
@callback
|
||||
def async_update_last_reset(
|
||||
self, node: ZwaveNode, endpoint: int, meter_type: int | None
|
||||
) -> None:
|
||||
"""Update last reset."""
|
||||
# If the signal is not for this node or is for a different endpoint, ignore it
|
||||
if self.info.node != node or self.info.primary_value.endpoint != endpoint:
|
||||
return
|
||||
# If a meter type was specified and doesn't match this entity's meter type,
|
||||
# ignore it
|
||||
if (
|
||||
meter_type is not None
|
||||
and self.info.primary_value.metadata.cc_specific.get("meterType")
|
||||
!= meter_type
|
||||
):
|
||||
return
|
||||
|
||||
self._attr_last_reset = dt.utcnow()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Restore the last reset time from stored state
|
||||
restored_state = await self.async_get_last_state()
|
||||
if restored_state and ATTR_LAST_RESET in restored_state.attributes:
|
||||
self._attr_last_reset = dt.parse_datetime(
|
||||
restored_state.attributes[ATTR_LAST_RESET]
|
||||
)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{SERVICE_RESET_METER}",
|
||||
self.async_update_last_reset,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_reset_meter(
|
||||
self, meter_type: int | None = None, value: int | None = None
|
||||
) -> None:
|
||||
"""Reset meter(s) on device."""
|
||||
node = self.info.node
|
||||
primary_value = self.info.primary_value
|
||||
if primary_value.command_class != CommandClass.METER:
|
||||
raise TypeError("Reset only available for Meter sensors")
|
||||
options = {}
|
||||
if meter_type is not None:
|
||||
options["type"] = meter_type
|
||||
@@ -253,6 +310,15 @@ class ZWaveNumericSensor(ZwaveSensorBase):
|
||||
primary_value.endpoint,
|
||||
options,
|
||||
)
|
||||
self._attr_last_reset = dt.utcnow()
|
||||
# Notify meters that may have been reset
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{SERVICE_RESET_METER}",
|
||||
node,
|
||||
primary_value.endpoint,
|
||||
options.get("type"),
|
||||
)
|
||||
|
||||
|
||||
class ZWaveListSensor(ZwaveSensorBase):
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Final
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 8
|
||||
PATCH_VERSION: Final = "0b1"
|
||||
PATCH_VERSION: Final = "0b4"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||
|
||||
@@ -28,7 +28,6 @@ FLOWS = [
|
||||
"atag",
|
||||
"august",
|
||||
"aurora",
|
||||
"automate",
|
||||
"awair",
|
||||
"axis",
|
||||
"azure_devops",
|
||||
|
||||
@@ -670,6 +670,7 @@ def async_config_entry_disabled_by_changed(
|
||||
the config entry is disabled, enable devices in the registry that are associated
|
||||
with a config entry when the config entry is enabled and the devices are marked
|
||||
DISABLED_CONFIG_ENTRY.
|
||||
Only disable a device if all associated config entries are disabled.
|
||||
"""
|
||||
|
||||
devices = async_entries_for_config_entry(registry, config_entry.entry_id)
|
||||
@@ -681,10 +682,20 @@ def async_config_entry_disabled_by_changed(
|
||||
registry.async_update_device(device.id, disabled_by=None)
|
||||
return
|
||||
|
||||
enabled_config_entries = {
|
||||
entry.entry_id
|
||||
for entry in registry.hass.config_entries.async_entries()
|
||||
if not entry.disabled_by
|
||||
}
|
||||
|
||||
for device in devices:
|
||||
if device.disabled:
|
||||
# Device already disabled, do not overwrite
|
||||
continue
|
||||
if len(device.config_entries) > 1 and device.config_entries.intersection(
|
||||
enabled_config_entries
|
||||
):
|
||||
continue
|
||||
registry.async_update_device(device.id, disabled_by=DISABLED_CONFIG_ENTRY)
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ defusedxml==0.7.1
|
||||
distro==1.5.0
|
||||
emoji==1.2.0
|
||||
hass-nabucasa==0.44.0
|
||||
home-assistant-frontend==20210728.0
|
||||
home-assistant-frontend==20210729.0
|
||||
httpx==0.18.2
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.0.1
|
||||
|
||||
@@ -220,9 +220,6 @@ aionotify==0.2.0
|
||||
# homeassistant.components.notion
|
||||
aionotion==3.0.2
|
||||
|
||||
# homeassistant.components.automate
|
||||
aiopulse2==0.6.0
|
||||
|
||||
# homeassistant.components.acmeda
|
||||
aiopulse==0.4.2
|
||||
|
||||
@@ -367,7 +364,7 @@ beautifulsoup4==4.9.3
|
||||
bellows==0.26.0
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.7.15
|
||||
bimmer_connected==0.7.16
|
||||
|
||||
# homeassistant.components.bizkaibus
|
||||
bizkaibus==0.1.1
|
||||
@@ -707,7 +704,7 @@ google-cloud-pubsub==2.1.0
|
||||
google-cloud-texttospeech==0.4.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==0.3.4
|
||||
google-nest-sdm==0.3.5
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -785,7 +782,7 @@ hole==0.5.1
|
||||
holidays==0.11.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210728.0
|
||||
home-assistant-frontend==20210729.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -1329,7 +1326,7 @@ pyatmo==5.2.3
|
||||
pyatome==0.1.1
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.8.1
|
||||
pyatv==0.8.2
|
||||
|
||||
# homeassistant.components.bbox
|
||||
pybbox==0.0.5-alpha
|
||||
@@ -1977,7 +1974,7 @@ pyvolumio==0.1.3
|
||||
pywebpush==1.9.2
|
||||
|
||||
# homeassistant.components.wemo
|
||||
pywemo==0.6.5
|
||||
pywemo==0.6.6
|
||||
|
||||
# homeassistant.components.wilight
|
||||
pywilight==0.0.70
|
||||
@@ -2468,7 +2465,7 @@ zigpy-zigate==0.7.3
|
||||
zigpy-znp==0.5.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.36.0
|
||||
zigpy==0.36.1
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
||||
@@ -142,9 +142,6 @@ aiomusiccast==0.8.2
|
||||
# homeassistant.components.notion
|
||||
aionotion==3.0.2
|
||||
|
||||
# homeassistant.components.automate
|
||||
aiopulse2==0.6.0
|
||||
|
||||
# homeassistant.components.acmeda
|
||||
aiopulse==0.4.2
|
||||
|
||||
@@ -223,7 +220,7 @@ base36==0.1.1
|
||||
bellows==0.26.0
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.7.15
|
||||
bimmer_connected==0.7.16
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==1.3.3
|
||||
@@ -404,7 +401,7 @@ google-api-python-client==1.6.4
|
||||
google-cloud-pubsub==2.1.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==0.3.4
|
||||
google-nest-sdm==0.3.5
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -452,7 +449,7 @@ hole==0.5.1
|
||||
holidays==0.11.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210728.0
|
||||
home-assistant-frontend==20210729.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -754,7 +751,7 @@ pyatag==0.3.5.3
|
||||
pyatmo==5.2.3
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.8.1
|
||||
pyatv==0.8.2
|
||||
|
||||
# homeassistant.components.blackbird
|
||||
pyblackbird==0.5
|
||||
@@ -1095,7 +1092,7 @@ pyvolumio==0.1.3
|
||||
pywebpush==1.9.2
|
||||
|
||||
# homeassistant.components.wemo
|
||||
pywemo==0.6.5
|
||||
pywemo==0.6.6
|
||||
|
||||
# homeassistant.components.wilight
|
||||
pywilight==0.0.70
|
||||
@@ -1365,7 +1362,7 @@ zigpy-zigate==0.7.3
|
||||
zigpy-znp==0.5.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.36.0
|
||||
zigpy==0.36.1
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.28.0
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Tests for the Automate Pulse Hub v2 integration."""
|
||||
@@ -1,69 +0,0 @@
|
||||
"""Test the Automate Pulse Hub v2 config flow."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components.automate.const import DOMAIN
|
||||
|
||||
|
||||
def mock_hub(testfunc=None):
|
||||
"""Mock aiopulse2.Hub."""
|
||||
Hub = Mock()
|
||||
Hub.name = "Name of the device"
|
||||
|
||||
async def hub_test():
|
||||
if testfunc:
|
||||
testfunc()
|
||||
|
||||
Hub.test = hub_test
|
||||
|
||||
return Hub
|
||||
|
||||
|
||||
async def test_form(hass):
|
||||
"""Test we get the form."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch("aiopulse2.Hub", return_value=mock_hub()), patch(
|
||||
"homeassistant.components.automate.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "Name of the device"
|
||||
assert result2["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass):
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
def raise_error():
|
||||
raise ConnectionRefusedError
|
||||
|
||||
with patch("aiopulse2.Hub", return_value=mock_hub(raise_error)):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
@@ -10,6 +10,7 @@ from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -118,7 +119,7 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket):
|
||||
|
||||
consumption_sensor = hass.states.get("sensor.consumption_sensor")
|
||||
assert consumption_sensor.state == "0.002"
|
||||
assert ATTR_DEVICE_CLASS not in consumption_sensor.attributes
|
||||
assert consumption_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY
|
||||
|
||||
assert not hass.states.get("sensor.clip_light_level_sensor")
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
|
||||
assert state.attributes[ATTR_LAST_RESET] == "1970-01-01T00:00:00+00:00"
|
||||
assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Total Energy"
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR
|
||||
assert ATTR_STATE_CLASS not in state.attributes
|
||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT
|
||||
|
||||
|
||||
async def test_turn_on(hass: HomeAssistant, fritz: Mock):
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT, TIME_SECONDS
|
||||
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
POWER_WATT,
|
||||
TIME_SECONDS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from tests.common import mock_restore_cache
|
||||
|
||||
async def test_state(hass):
|
||||
|
||||
async def test_state(hass) -> None:
|
||||
"""Test integration sensor state."""
|
||||
config = {
|
||||
"sensor": {
|
||||
@@ -19,15 +29,25 @@ async def test_state(hass):
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, "sensor", config)
|
||||
|
||||
entity_id = config["sensor"]["source"]
|
||||
hass.states.async_set(entity_id, 1, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
now = dt_util.utcnow() + timedelta(seconds=3600)
|
||||
now = dt_util.utcnow()
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||
hass.states.async_set(entity_id, 1, {}, force_update=True)
|
||||
assert await async_setup_component(hass, "sensor", config)
|
||||
|
||||
entity_id = config["sensor"]["source"]
|
||||
hass.states.async_set(entity_id, 1, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.integration")
|
||||
assert state is not None
|
||||
assert state.attributes.get("last_reset") == now.isoformat()
|
||||
assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT
|
||||
assert "device_class" not in state.attributes
|
||||
|
||||
future_now = dt_util.utcnow() + timedelta(seconds=3600)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=future_now):
|
||||
hass.states.async_set(
|
||||
entity_id, 1, {"device_class": DEVICE_CLASS_POWER}, force_update=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.integration")
|
||||
@@ -37,6 +57,82 @@ async def test_state(hass):
|
||||
assert round(float(state.state), config["sensor"]["round"]) == 1.0
|
||||
|
||||
assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR
|
||||
assert state.attributes.get("device_class") == DEVICE_CLASS_ENERGY
|
||||
assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT
|
||||
assert state.attributes.get("last_reset") == now.isoformat()
|
||||
|
||||
|
||||
async def test_restore_state(hass: HomeAssistant) -> None:
|
||||
"""Test integration sensor state is restored correctly."""
|
||||
mock_restore_cache(
|
||||
hass,
|
||||
(
|
||||
State(
|
||||
"sensor.integration",
|
||||
"100.0",
|
||||
{
|
||||
"last_reset": "2019-10-06T21:00:00",
|
||||
"device_class": DEVICE_CLASS_ENERGY,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
config = {
|
||||
"sensor": {
|
||||
"platform": "integration",
|
||||
"name": "integration",
|
||||
"source": "sensor.power",
|
||||
"unit": ENERGY_KILO_WATT_HOUR,
|
||||
"round": 2,
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, "sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.integration")
|
||||
assert state
|
||||
assert state.state == "100.00"
|
||||
assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR
|
||||
assert state.attributes.get("device_class") == DEVICE_CLASS_ENERGY
|
||||
assert state.attributes.get("last_reset") == "2019-10-06T21:00:00"
|
||||
|
||||
|
||||
async def test_restore_state_failed(hass: HomeAssistant) -> None:
|
||||
"""Test integration sensor state is restored correctly."""
|
||||
mock_restore_cache(
|
||||
hass,
|
||||
(
|
||||
State(
|
||||
"sensor.integration",
|
||||
"INVALID",
|
||||
{
|
||||
"last_reset": "2019-10-06T21:00:00.000000",
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
config = {
|
||||
"sensor": {
|
||||
"platform": "integration",
|
||||
"name": "integration",
|
||||
"source": "sensor.power",
|
||||
"unit": ENERGY_KILO_WATT_HOUR,
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, "sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.integration")
|
||||
assert state
|
||||
assert state.state == "0"
|
||||
assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR
|
||||
assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT
|
||||
assert state.attributes.get("last_reset") != "2019-10-06T21:00:00"
|
||||
assert "device_class" not in state.attributes
|
||||
|
||||
|
||||
async def test_trapezoidal(hass):
|
||||
|
||||
@@ -8,7 +8,7 @@ from tests.common import MockConfigEntry
|
||||
CONTRACT = "1234abcd"
|
||||
|
||||
|
||||
async def setup_platform(hass, platform):
|
||||
async def setup_platform(hass):
|
||||
"""Set up the Prosegur platform."""
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=PROSEGUR_DOMAIN,
|
||||
|
||||
@@ -48,7 +48,7 @@ def mock_status(request):
|
||||
|
||||
async def test_entity_registry(hass, mock_auth, mock_status):
|
||||
"""Tests that the devices are registered in the entity registry."""
|
||||
await setup_platform(hass, ALARM_DOMAIN)
|
||||
await setup_platform(hass)
|
||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
entry = entity_registry.async_get(PROSEGUR_ALARM_ENTITY)
|
||||
@@ -74,7 +74,7 @@ async def test_connection_error(hass, mock_auth):
|
||||
|
||||
with patch("pyprosegur.installation.Installation.retrieve", return_value=install):
|
||||
|
||||
await setup_platform(hass, ALARM_DOMAIN)
|
||||
await setup_platform(hass)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -106,7 +106,7 @@ async def test_arm(hass, mock_auth, code, alarm_service, alarm_state):
|
||||
install.status = code
|
||||
|
||||
with patch("pyprosegur.installation.Installation.retrieve", return_value=install):
|
||||
await setup_platform(hass, ALARM_DOMAIN)
|
||||
await setup_platform(hass)
|
||||
|
||||
await hass.services.async_call(
|
||||
ALARM_DOMAIN,
|
||||
|
||||
@@ -26,7 +26,7 @@ async def test_form(hass):
|
||||
with patch(
|
||||
"homeassistant.components.prosegur.config_flow.Installation.retrieve",
|
||||
return_value=install,
|
||||
), patch(
|
||||
) as mock_retrieve, patch(
|
||||
"homeassistant.components.prosegur.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
@@ -50,6 +50,8 @@ async def test_form(hass):
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
assert len(mock_retrieve.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass):
|
||||
"""Test we handle invalid auth."""
|
||||
@@ -120,28 +122,6 @@ async def test_form_unknown_exception(hass):
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_validate_input(hass):
|
||||
"""Test we retrieve data from Installation."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"pyprosegur.installation.Installation.retrieve",
|
||||
return_value=MagicMock,
|
||||
) as mock_retrieve:
|
||||
await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"country": "PT",
|
||||
},
|
||||
)
|
||||
|
||||
assert len(mock_retrieve.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_reauth_flow(hass):
|
||||
"""Test a reauthentication flow."""
|
||||
entry = MockConfigEntry(
|
||||
|
||||
@@ -18,7 +18,6 @@ from tests.common import MockConfigEntry
|
||||
async def test_setup_entry_fail_retrieve(hass, error):
|
||||
"""Test loading the Prosegur entry."""
|
||||
|
||||
hass.config.components.add(DOMAIN)
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
@@ -47,7 +46,6 @@ async def test_unload_entry(hass, aioclient_mock):
|
||||
json={"data": {"token": "123456789"}},
|
||||
)
|
||||
|
||||
hass.config.components.add(DOMAIN)
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
|
||||
@@ -6,7 +6,11 @@ real HTTP calls are not initiated during testing.
|
||||
"""
|
||||
from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability
|
||||
|
||||
from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.sensor import (
|
||||
DEVICE_CLASSES,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
STATE_CLASSES,
|
||||
)
|
||||
from homeassistant.components.smartthings import sensor
|
||||
from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -33,6 +37,8 @@ async def test_mapping_integrity():
|
||||
assert (
|
||||
sensor_map.device_class in DEVICE_CLASSES
|
||||
), sensor_map.device_class
|
||||
if sensor_map.state_class:
|
||||
assert sensor_map.state_class in STATE_CLASSES, sensor_map.state_class
|
||||
|
||||
|
||||
async def test_entity_state(hass, device_factory):
|
||||
@@ -95,6 +101,44 @@ async def test_entity_and_device_attributes(hass, device_factory):
|
||||
assert entry.manufacturer == "Unavailable"
|
||||
|
||||
|
||||
async def test_energy_sensors_for_switch_device(hass, device_factory):
|
||||
"""Test the attributes of the entity are correct."""
|
||||
# Arrange
|
||||
device = device_factory(
|
||||
"Switch_1",
|
||||
[Capability.switch, Capability.power_meter, Capability.energy_meter],
|
||||
{Attribute.switch: "off", Attribute.power: 355, Attribute.energy: 11.422},
|
||||
)
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
# Act
|
||||
await setup_platform(hass, SENSOR_DOMAIN, devices=[device])
|
||||
# Assert
|
||||
state = hass.states.get("sensor.switch_1_energy_meter")
|
||||
assert state
|
||||
assert state.state == "11.422"
|
||||
entry = entity_registry.async_get("sensor.switch_1_energy_meter")
|
||||
assert entry
|
||||
assert entry.unique_id == f"{device.device_id}.{Attribute.energy}"
|
||||
entry = device_registry.async_get_device({(DOMAIN, device.device_id)})
|
||||
assert entry
|
||||
assert entry.name == device.label
|
||||
assert entry.model == device.device_type_name
|
||||
assert entry.manufacturer == "Unavailable"
|
||||
|
||||
state = hass.states.get("sensor.switch_1_power_meter")
|
||||
assert state
|
||||
assert state.state == "355"
|
||||
entry = entity_registry.async_get("sensor.switch_1_power_meter")
|
||||
assert entry
|
||||
assert entry.unique_id == f"{device.device_id}.{Attribute.power}"
|
||||
entry = device_registry.async_get_device({(DOMAIN, device.device_id)})
|
||||
assert entry
|
||||
assert entry.name == device.label
|
||||
assert entry.model == device.device_type_name
|
||||
assert entry.manufacturer == "Unavailable"
|
||||
|
||||
|
||||
async def test_update_from_signal(hass, device_factory):
|
||||
"""Test the binary_sensor updates when receiving a signal."""
|
||||
# Arrange
|
||||
|
||||
@@ -7,11 +7,7 @@ real HTTP calls are not initiated during testing.
|
||||
from pysmartthings import Attribute, Capability
|
||||
|
||||
from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE
|
||||
from homeassistant.components.switch import (
|
||||
ATTR_CURRENT_POWER_W,
|
||||
ATTR_TODAY_ENERGY_KWH,
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
@@ -72,8 +68,6 @@ async def test_turn_on(hass, device_factory):
|
||||
state = hass.states.get("switch.switch_1")
|
||||
assert state is not None
|
||||
assert state.state == "on"
|
||||
assert state.attributes[ATTR_CURRENT_POWER_W] == 355
|
||||
assert state.attributes[ATTR_TODAY_ENERGY_KWH] == 11.422
|
||||
|
||||
|
||||
async def test_update_from_signal(hass, device_factory):
|
||||
|
||||
@@ -790,3 +790,65 @@ async def test_async_detect_interfaces_setting_empty_route(hass):
|
||||
(IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")),
|
||||
(IPv4Address("192.168.1.5"), None),
|
||||
}
|
||||
|
||||
|
||||
async def test_bind_failure_skips_adapter(hass, caplog):
|
||||
"""Test that an adapter with a bind failure is skipped."""
|
||||
mock_get_ssdp = {
|
||||
"mock-domain": [
|
||||
{
|
||||
ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC",
|
||||
}
|
||||
]
|
||||
}
|
||||
create_args = []
|
||||
did_search = 0
|
||||
|
||||
@callback
|
||||
def _callback(*_):
|
||||
nonlocal did_search
|
||||
did_search += 1
|
||||
pass
|
||||
|
||||
def _generate_failing_ssdp_listener(*args, **kwargs):
|
||||
create_args.append([args, kwargs])
|
||||
listener = SSDPListener(*args, **kwargs)
|
||||
|
||||
async def _async_callback(*_):
|
||||
if kwargs["source_ip"] == IPv6Address("2001:db8::"):
|
||||
raise OSError
|
||||
pass
|
||||
|
||||
listener.async_start = _async_callback
|
||||
listener.async_search = _callback
|
||||
return listener
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ssdp.async_get_ssdp",
|
||||
return_value=mock_get_ssdp,
|
||||
), patch(
|
||||
"homeassistant.components.ssdp.SSDPListener",
|
||||
new=_generate_failing_ssdp_listener,
|
||||
), patch(
|
||||
"homeassistant.components.ssdp.network.async_get_adapters",
|
||||
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
||||
):
|
||||
assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
argset = set()
|
||||
for argmap in create_args:
|
||||
argset.add((argmap[1].get("source_ip"), argmap[1].get("target_ip")))
|
||||
|
||||
assert argset == {
|
||||
(IPv6Address("2001:db8::"), None),
|
||||
(IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")),
|
||||
(IPv4Address("192.168.1.5"), None),
|
||||
}
|
||||
assert "Failed to setup listener for" in caplog.text
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
||||
await hass.async_block_till_done()
|
||||
assert did_search == 2
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Constants for the TP-Link component tests."""
|
||||
|
||||
SMARTPLUGSWITCH_DATA = {
|
||||
"sysinfo": {
|
||||
"sw_ver": "1.0.4 Build 191111 Rel.143500",
|
||||
"hw_ver": "4.0",
|
||||
"model": "HS110(EU)",
|
||||
"deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581",
|
||||
"oemId": "40F54B43071E9436B6395611E9D91CEA",
|
||||
"hwId": "A6C77E4FDD238B53D824AC8DA361F043",
|
||||
"rssi": -24,
|
||||
"longitude_i": 130793,
|
||||
"latitude_i": 480582,
|
||||
"alias": "SmartPlug",
|
||||
"status": "new",
|
||||
"mic_type": "IOT.SMARTPLUGSWITCH",
|
||||
"feature": "TIM:ENE",
|
||||
"mac": "69:F2:3C:8E:E3:47",
|
||||
"updating": 0,
|
||||
"led_off": 0,
|
||||
"relay_state": 0,
|
||||
"on_time": 0,
|
||||
"active_mode": "none",
|
||||
"icon_hash": "",
|
||||
"dev_name": "Smart Wi-Fi Plug With Energy Monitoring",
|
||||
"next_action": {"type": -1},
|
||||
"err_code": 0,
|
||||
},
|
||||
"realtime": {
|
||||
"voltage_mv": 233957,
|
||||
"current_ma": 21,
|
||||
"power_mw": 0,
|
||||
"total_wh": 1793,
|
||||
"err_code": 0,
|
||||
},
|
||||
}
|
||||
SMARTSTRIPWITCH_DATA = {
|
||||
"sysinfo": {
|
||||
"sw_ver": "1.0.4 Build 191111 Rel.143500",
|
||||
"hw_ver": "4.0",
|
||||
"model": "HS110(EU)",
|
||||
"deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581",
|
||||
"oemId": "40F54B43071E9436B6395611E9D91CEA",
|
||||
"hwId": "A6C77E4FDD238B53D824AC8DA361F043",
|
||||
"rssi": -24,
|
||||
"longitude_i": 130793,
|
||||
"latitude_i": 480582,
|
||||
"alias": "SmartPlug",
|
||||
"status": "new",
|
||||
"mic_type": "IOT.SMARTPLUGSWITCH",
|
||||
"feature": "TIM",
|
||||
"mac": "69:F2:3C:8E:E3:47",
|
||||
"updating": 0,
|
||||
"led_off": 0,
|
||||
"relay_state": 0,
|
||||
"on_time": 0,
|
||||
"active_mode": "none",
|
||||
"icon_hash": "",
|
||||
"dev_name": "Smart Wi-Fi Plug With Energy Monitoring",
|
||||
"next_action": {"type": -1},
|
||||
"children": [{"id": "1", "state": 1, "alias": "SmartPlug#1"}],
|
||||
"err_code": 0,
|
||||
},
|
||||
"realtime": {
|
||||
"voltage_mv": 233957,
|
||||
"current_ma": 21,
|
||||
"power_mw": 0,
|
||||
"total_wh": 1793,
|
||||
"err_code": 0,
|
||||
},
|
||||
"context": "1",
|
||||
}
|
||||
@@ -1,24 +1,34 @@
|
||||
"""Tests for the TP-Link component."""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug
|
||||
from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug, smartstrip
|
||||
from pyHS100.smartdevice import EmeterStatus
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components import tplink
|
||||
from homeassistant.components.tplink.common import (
|
||||
from homeassistant.components.tplink.common import SmartDevices
|
||||
from homeassistant.components.tplink.const import (
|
||||
CONF_DIMMER,
|
||||
CONF_DISCOVERY,
|
||||
CONF_LIGHT,
|
||||
CONF_SW_VERSION,
|
||||
CONF_SWITCH,
|
||||
COORDINATORS,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.components.tplink.sensor import ENERGY_SENSORS
|
||||
from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from tests.common import MockConfigEntry, mock_coro
|
||||
from tests.components.tplink.consts import SMARTPLUGSWITCH_DATA, SMARTSTRIPWITCH_DATA
|
||||
|
||||
|
||||
async def test_creating_entry_tries_discover(hass):
|
||||
@@ -186,7 +196,7 @@ async def test_configuring_discovery_disabled(hass):
|
||||
assert mock_setup.call_count == 1
|
||||
|
||||
|
||||
async def test_platforms_are_initialized(hass):
|
||||
async def test_platforms_are_initialized(hass: HomeAssistant):
|
||||
"""Test that platforms are initialized per configuration array."""
|
||||
config = {
|
||||
tplink.DOMAIN: {
|
||||
@@ -196,26 +206,132 @@ async def test_platforms_are_initialized(hass):
|
||||
}
|
||||
}
|
||||
|
||||
with patch("homeassistant.components.tplink.common.Discover.discover"), patch(
|
||||
"homeassistant.components.tplink.get_static_devices"
|
||||
) as get_static_devices, patch(
|
||||
"homeassistant.components.tplink.common.SmartDevice._query_helper"
|
||||
), patch(
|
||||
"homeassistant.components.tplink.light.async_setup_entry",
|
||||
return_value=mock_coro(True),
|
||||
), patch(
|
||||
"homeassistant.components.tplink.common.SmartPlug.is_dimmable",
|
||||
False,
|
||||
):
|
||||
|
||||
light = SmartBulb("123.123.123.123")
|
||||
switch = SmartPlug("321.321.321.321")
|
||||
switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"])
|
||||
switch.get_emeter_realtime = MagicMock(
|
||||
return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"])
|
||||
)
|
||||
switch.get_emeter_daily = MagicMock(
|
||||
return_value={int(time.strftime("%e")): 1.123}
|
||||
)
|
||||
get_static_devices.return_value = SmartDevices([light], [switch])
|
||||
|
||||
# patching is_dimmable is necessray to avoid misdetection as light.
|
||||
await async_setup_component(hass, tplink.DOMAIN, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(f"switch.{switch.alias}")
|
||||
assert state
|
||||
assert state.name == switch.alias
|
||||
|
||||
for description in ENERGY_SENSORS:
|
||||
state = hass.states.get(
|
||||
f"sensor.{switch.alias}_{slugify(description.name)}"
|
||||
)
|
||||
assert state
|
||||
assert state.state is not None
|
||||
assert state.name == f"{switch.alias} {description.name}"
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
assert len(device_registry.devices) == 1
|
||||
device = next(iter(device_registry.devices.values()))
|
||||
assert device.name == switch.alias
|
||||
assert device.model == switch.model
|
||||
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, switch.mac.lower())}
|
||||
assert device.sw_version == switch.sys_info[CONF_SW_VERSION]
|
||||
|
||||
|
||||
async def test_smartplug_without_consumption_sensors(hass: HomeAssistant):
|
||||
"""Test that platforms are initialized per configuration array."""
|
||||
config = {
|
||||
tplink.DOMAIN: {
|
||||
CONF_DISCOVERY: False,
|
||||
CONF_SWITCH: [{CONF_HOST: "321.321.321.321"}],
|
||||
}
|
||||
}
|
||||
|
||||
with patch("homeassistant.components.tplink.common.Discover.discover"), patch(
|
||||
"homeassistant.components.tplink.get_static_devices"
|
||||
) as get_static_devices, patch(
|
||||
"homeassistant.components.tplink.common.SmartDevice._query_helper"
|
||||
), patch(
|
||||
"homeassistant.components.tplink.light.async_setup_entry",
|
||||
return_value=mock_coro(True),
|
||||
), patch(
|
||||
"homeassistant.components.tplink.switch.async_setup_entry",
|
||||
return_value=mock_coro(True),
|
||||
), patch(
|
||||
"homeassistant.components.tplink.common.SmartPlug.is_dimmable", False
|
||||
):
|
||||
|
||||
switch = SmartPlug("321.321.321.321")
|
||||
switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"])
|
||||
get_static_devices.return_value = SmartDevices([], [switch])
|
||||
|
||||
await async_setup_component(hass, tplink.DOMAIN, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for description in ENERGY_SENSORS:
|
||||
state = hass.states.get(
|
||||
f"sensor.{switch.alias}_{slugify(description.name)}"
|
||||
)
|
||||
assert state is None
|
||||
|
||||
|
||||
async def test_smartstrip_device(hass: HomeAssistant):
|
||||
"""Test discover a SmartStrip devices."""
|
||||
config = {
|
||||
tplink.DOMAIN: {
|
||||
CONF_DISCOVERY: True,
|
||||
}
|
||||
}
|
||||
|
||||
class SmartStrip(smartstrip.SmartStrip):
|
||||
"""Moked SmartStrip class."""
|
||||
|
||||
def get_sysinfo(self):
|
||||
return SMARTSTRIPWITCH_DATA["sysinfo"]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tplink.common.Discover.discover"
|
||||
) as discover, patch(
|
||||
"homeassistant.components.tplink.common.SmartDevice._query_helper"
|
||||
), patch(
|
||||
"homeassistant.components.tplink.light.async_setup_entry",
|
||||
return_value=mock_coro(True),
|
||||
) as light_setup, patch(
|
||||
"homeassistant.components.tplink.switch.async_setup_entry",
|
||||
return_value=mock_coro(True),
|
||||
) as switch_setup, patch(
|
||||
"homeassistant.components.tplink.common.SmartPlug.is_dimmable", False
|
||||
"homeassistant.components.tplink.common.SmartPlug.get_sysinfo",
|
||||
return_value=SMARTSTRIPWITCH_DATA["sysinfo"],
|
||||
), patch(
|
||||
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setup"
|
||||
):
|
||||
# patching is_dimmable is necessray to avoid misdetection as light.
|
||||
await async_setup_component(hass, tplink.DOMAIN, config)
|
||||
|
||||
strip = SmartStrip("123.123.123.123")
|
||||
discover.return_value = {"123.123.123.123": strip}
|
||||
|
||||
assert await async_setup_component(hass, tplink.DOMAIN, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert discover.call_count == 0
|
||||
assert light_setup.call_count == 1
|
||||
assert switch_setup.call_count == 1
|
||||
assert hass.data.get(tplink.DOMAIN)
|
||||
assert hass.data[tplink.DOMAIN].get(COORDINATORS)
|
||||
assert hass.data[tplink.DOMAIN][COORDINATORS].get(strip.mac)
|
||||
assert isinstance(
|
||||
hass.data[tplink.DOMAIN][COORDINATORS][strip.mac],
|
||||
tplink.SmartPlugDataUpdateCoordinator,
|
||||
)
|
||||
data = hass.data[tplink.DOMAIN][COORDINATORS][strip.mac].data
|
||||
assert data[CONF_ALIAS] == strip.sys_info["children"][0]["alias"]
|
||||
assert data[CONF_DEVICE_ID] == "1"
|
||||
|
||||
|
||||
async def test_no_config_creates_no_entry(hass):
|
||||
@@ -230,6 +346,42 @@ async def test_no_config_creates_no_entry(hass):
|
||||
assert mock_setup.call_count == 0
|
||||
|
||||
|
||||
async def test_not_ready(hass: HomeAssistant):
|
||||
"""Test for not ready when configured devices are not available."""
|
||||
config = {
|
||||
tplink.DOMAIN: {
|
||||
CONF_DISCOVERY: False,
|
||||
CONF_SWITCH: [{CONF_HOST: "321.321.321.321"}],
|
||||
}
|
||||
}
|
||||
|
||||
with patch("homeassistant.components.tplink.common.Discover.discover"), patch(
|
||||
"homeassistant.components.tplink.get_static_devices"
|
||||
) as get_static_devices, patch(
|
||||
"homeassistant.components.tplink.common.SmartDevice._query_helper"
|
||||
), patch(
|
||||
"homeassistant.components.tplink.light.async_setup_entry",
|
||||
return_value=mock_coro(True),
|
||||
), patch(
|
||||
"homeassistant.components.tplink.switch.async_setup_entry",
|
||||
return_value=mock_coro(True),
|
||||
), patch(
|
||||
"homeassistant.components.tplink.common.SmartPlug.is_dimmable", False
|
||||
):
|
||||
|
||||
switch = SmartPlug("321.321.321.321")
|
||||
switch.get_sysinfo = MagicMock(side_effect=SmartDeviceException())
|
||||
get_static_devices.return_value = SmartDevices([], [switch])
|
||||
|
||||
await async_setup_component(hass, tplink.DOMAIN, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entries = hass.config_entries.async_entries(tplink.DOMAIN)
|
||||
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state is config_entries.ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ["switch", "light"])
|
||||
async def test_unload(hass, platform):
|
||||
"""Test that the async_unload_entry works."""
|
||||
@@ -238,21 +390,35 @@ async def test_unload(hass, platform):
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tplink.get_static_devices"
|
||||
) as get_static_devices, patch(
|
||||
"homeassistant.components.tplink.common.SmartDevice._query_helper"
|
||||
), patch(
|
||||
f"homeassistant.components.tplink.{platform}.async_setup_entry",
|
||||
return_value=mock_coro(True),
|
||||
) as light_setup:
|
||||
) as async_setup_entry:
|
||||
config = {
|
||||
tplink.DOMAIN: {
|
||||
platform: [{CONF_HOST: "123.123.123.123"}],
|
||||
CONF_DISCOVERY: False,
|
||||
}
|
||||
}
|
||||
|
||||
light = SmartBulb("123.123.123.123")
|
||||
switch = SmartPlug("321.321.321.321")
|
||||
switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"])
|
||||
switch.get_emeter_realtime = MagicMock(
|
||||
return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"])
|
||||
)
|
||||
if platform == "light":
|
||||
get_static_devices.return_value = SmartDevices([light], [])
|
||||
elif platform == "switch":
|
||||
get_static_devices.return_value = SmartDevices([], [switch])
|
||||
|
||||
assert await async_setup_component(hass, tplink.DOMAIN, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(light_setup.mock_calls) == 1
|
||||
assert len(async_setup_entry.mock_calls) == 1
|
||||
assert tplink.DOMAIN in hass.data
|
||||
|
||||
assert await tplink.async_unload_entry(hass, entry)
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.components.light import (
|
||||
ATTR_HS_COLOR,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.tplink.common import (
|
||||
from homeassistant.components.tplink.const import (
|
||||
CONF_DIMMER,
|
||||
CONF_DISCOVERY,
|
||||
CONF_LIGHT,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
"""Provide common test tools for Z-Wave JS."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature"
|
||||
HUMIDITY_SENSOR = "sensor.multisensor_6_humidity"
|
||||
ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2"
|
||||
POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
|
||||
ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2"
|
||||
VOLTAGE_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_3"
|
||||
CURRENT_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_4"
|
||||
SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports"
|
||||
LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level"
|
||||
ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any"
|
||||
@@ -11,6 +15,8 @@ NOTIFICATION_MOTION_BINARY_SENSOR = (
|
||||
"binary_sensor.multisensor_6_home_security_motion_detection"
|
||||
)
|
||||
NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_status"
|
||||
INDICATOR_SENSOR = "sensor.z_wave_thermostat_indicator_value"
|
||||
BASIC_SENSOR = "sensor.livingroomlight_basic"
|
||||
PROPERTY_DOOR_STATUS_BINARY_SENSOR = (
|
||||
"binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door"
|
||||
)
|
||||
@@ -27,3 +33,7 @@ ID_LOCK_CONFIG_PARAMETER_SENSOR = (
|
||||
"sensor.z_wave_module_for_id_lock_150_and_101_config_parameter_door_lock_mode"
|
||||
)
|
||||
ZEN_31_ENTITY = "light.kitchen_under_cabinet_lights"
|
||||
METER_SENSOR = "sensor.smart_switch_6_electric_consumed_v"
|
||||
|
||||
DATETIME_ZERO = datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||
DATETIME_LAST_RESET = datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
@@ -11,6 +11,11 @@ from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.node import Node
|
||||
from zwave_js_server.version import VersionInfo
|
||||
|
||||
from homeassistant.components.sensor import ATTR_LAST_RESET
|
||||
from homeassistant.core import State
|
||||
|
||||
from .common import DATETIME_LAST_RESET
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
# Add-on fixtures
|
||||
@@ -835,3 +840,16 @@ def aeotec_zw164_siren_fixture(client, aeotec_zw164_siren_state):
|
||||
def firmware_file_fixture():
|
||||
"""Return mock firmware file stream."""
|
||||
return io.BytesIO(bytes(10))
|
||||
|
||||
|
||||
@pytest.fixture(name="restore_last_reset")
|
||||
def restore_last_reset_fixture():
|
||||
"""Return mock restore last reset."""
|
||||
state = State(
|
||||
"sensor.test", "test", {ATTR_LAST_RESET: DATETIME_LAST_RESET.isoformat()}
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.zwave_js.sensor.ZWaveMeterSensor.async_get_last_state",
|
||||
return_value=state,
|
||||
):
|
||||
yield state
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""Test the Z-Wave JS sensor platform."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from zwave_js_server.event import Event
|
||||
|
||||
from homeassistant.components.sensor import ATTR_LAST_RESET, STATE_CLASS_MEASUREMENT
|
||||
from homeassistant.components.zwave_js.const import (
|
||||
ATTR_METER_TYPE,
|
||||
ATTR_VALUE,
|
||||
@@ -9,10 +12,14 @@ from homeassistant.components.zwave_js.const import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ELECTRIC_CURRENT_AMPERE,
|
||||
ELECTRIC_POTENTIAL_VOLT,
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
POWER_WATT,
|
||||
TEMP_CELSIUS,
|
||||
@@ -21,11 +28,18 @@ from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .common import (
|
||||
AIR_TEMPERATURE_SENSOR,
|
||||
BASIC_SENSOR,
|
||||
CURRENT_SENSOR,
|
||||
DATETIME_LAST_RESET,
|
||||
DATETIME_ZERO,
|
||||
ENERGY_SENSOR,
|
||||
HUMIDITY_SENSOR,
|
||||
ID_LOCK_CONFIG_PARAMETER_SENSOR,
|
||||
INDICATOR_SENSOR,
|
||||
METER_SENSOR,
|
||||
NOTIFICATION_MOTION_SENSOR,
|
||||
POWER_SENSOR,
|
||||
VOLTAGE_SENSOR,
|
||||
)
|
||||
|
||||
|
||||
@@ -54,6 +68,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration):
|
||||
assert state.state == "0.0"
|
||||
assert state.attributes["unit_of_measurement"] == POWER_WATT
|
||||
assert state.attributes["device_class"] == DEVICE_CLASS_POWER
|
||||
assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT
|
||||
|
||||
state = hass.states.get(ENERGY_SENSOR)
|
||||
|
||||
@@ -61,6 +76,21 @@ async def test_energy_sensors(hass, hank_binary_switch, integration):
|
||||
assert state.state == "0.16"
|
||||
assert state.attributes["unit_of_measurement"] == ENERGY_KILO_WATT_HOUR
|
||||
assert state.attributes["device_class"] == DEVICE_CLASS_ENERGY
|
||||
assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT
|
||||
|
||||
state = hass.states.get(VOLTAGE_SENSOR)
|
||||
|
||||
assert state
|
||||
assert state.state == "122.96"
|
||||
assert state.attributes["unit_of_measurement"] == ELECTRIC_POTENTIAL_VOLT
|
||||
assert state.attributes["device_class"] == DEVICE_CLASS_VOLTAGE
|
||||
|
||||
state = hass.states.get(CURRENT_SENSOR)
|
||||
|
||||
assert state
|
||||
assert state.state == "0.0"
|
||||
assert state.attributes["unit_of_measurement"] == ELECTRIC_CURRENT_AMPERE
|
||||
assert state.attributes["device_class"] == DEVICE_CLASS_CURRENT
|
||||
|
||||
|
||||
async def test_disabled_notification_sensor(hass, multisensor_6, integration):
|
||||
@@ -88,6 +118,28 @@ async def test_disabled_notification_sensor(hass, multisensor_6, integration):
|
||||
assert state.attributes["value"] == 8
|
||||
|
||||
|
||||
async def test_disabled_indcator_sensor(
|
||||
hass, climate_radio_thermostat_ct100_plus, integration
|
||||
):
|
||||
"""Test sensor is created from Indicator CC and is disabled."""
|
||||
ent_reg = er.async_get(hass)
|
||||
entity_entry = ent_reg.async_get(INDICATOR_SENSOR)
|
||||
|
||||
assert entity_entry
|
||||
assert entity_entry.disabled
|
||||
assert entity_entry.disabled_by == er.DISABLED_INTEGRATION
|
||||
|
||||
|
||||
async def test_disabled_basic_sensor(hass, ge_in_wall_dimmer_switch, integration):
|
||||
"""Test sensor is created from Basic CC and is disabled."""
|
||||
ent_reg = er.async_get(hass)
|
||||
entity_entry = ent_reg.async_get(BASIC_SENSOR)
|
||||
|
||||
assert entity_entry
|
||||
assert entity_entry.disabled
|
||||
assert entity_entry.disabled_by == er.DISABLED_INTEGRATION
|
||||
|
||||
|
||||
async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration):
|
||||
"""Test config parameter sensor is created."""
|
||||
ent_reg = er.async_get(hass)
|
||||
@@ -147,18 +199,30 @@ async def test_reset_meter(
|
||||
integration,
|
||||
):
|
||||
"""Test reset_meter service."""
|
||||
SENSOR = "sensor.smart_switch_6_electric_consumed_v"
|
||||
client.async_send_command.return_value = {}
|
||||
client.async_send_command_no_wait.return_value = {}
|
||||
|
||||
# Test successful meter reset call
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_RESET_METER,
|
||||
{
|
||||
ATTR_ENTITY_ID: SENSOR,
|
||||
},
|
||||
blocking=True,
|
||||
# Validate that the sensor last reset is starting from nothing
|
||||
assert (
|
||||
hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET]
|
||||
== DATETIME_ZERO.isoformat()
|
||||
)
|
||||
|
||||
# Test successful meter reset call, patching utcnow so we can make sure the last
|
||||
# reset gets updated
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=DATETIME_LAST_RESET):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_RESET_METER,
|
||||
{
|
||||
ATTR_ENTITY_ID: METER_SENSOR,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert (
|
||||
hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET]
|
||||
== DATETIME_LAST_RESET.isoformat()
|
||||
)
|
||||
|
||||
assert len(client.async_send_command_no_wait.call_args_list) == 1
|
||||
@@ -175,7 +239,7 @@ async def test_reset_meter(
|
||||
DOMAIN,
|
||||
SERVICE_RESET_METER,
|
||||
{
|
||||
ATTR_ENTITY_ID: SENSOR,
|
||||
ATTR_ENTITY_ID: METER_SENSOR,
|
||||
ATTR_METER_TYPE: 1,
|
||||
ATTR_VALUE: 2,
|
||||
},
|
||||
@@ -190,3 +254,17 @@ async def test_reset_meter(
|
||||
assert args["args"] == [{"type": 1, "targetValue": 2}]
|
||||
|
||||
client.async_send_command_no_wait.reset_mock()
|
||||
|
||||
|
||||
async def test_restore_last_reset(
|
||||
hass,
|
||||
client,
|
||||
aeon_smart_switch_6,
|
||||
restore_last_reset,
|
||||
integration,
|
||||
):
|
||||
"""Test restoring last_reset on setup."""
|
||||
assert (
|
||||
hass.states.get(METER_SENSOR).attributes[ATTR_LAST_RESET]
|
||||
== DATETIME_LAST_RESET.isoformat()
|
||||
)
|
||||
|
||||
@@ -1253,3 +1253,45 @@ async def test_disable_config_entry_disables_devices(hass, registry):
|
||||
entry2 = registry.async_get(entry2.id)
|
||||
assert entry2.disabled
|
||||
assert entry2.disabled_by == device_registry.DISABLED_USER
|
||||
|
||||
|
||||
async def test_only_disable_device_if_all_config_entries_are_disabled(hass, registry):
|
||||
"""Test that we only disable device if all related config entries are disabled."""
|
||||
config_entry1 = MockConfigEntry(domain="light")
|
||||
config_entry1.add_to_hass(hass)
|
||||
config_entry2 = MockConfigEntry(domain="light")
|
||||
config_entry2.add_to_hass(hass)
|
||||
|
||||
registry.async_get_or_create(
|
||||
config_entry_id=config_entry1.entry_id,
|
||||
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
)
|
||||
entry1 = registry.async_get_or_create(
|
||||
config_entry_id=config_entry2.entry_id,
|
||||
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
)
|
||||
assert len(entry1.config_entries) == 2
|
||||
assert not entry1.disabled
|
||||
|
||||
await hass.config_entries.async_set_disabled_by(
|
||||
config_entry1.entry_id, config_entries.DISABLED_USER
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entry1 = registry.async_get(entry1.id)
|
||||
assert not entry1.disabled
|
||||
|
||||
await hass.config_entries.async_set_disabled_by(
|
||||
config_entry2.entry_id, config_entries.DISABLED_USER
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entry1 = registry.async_get(entry1.id)
|
||||
assert entry1.disabled
|
||||
assert entry1.disabled_by == device_registry.DISABLED_CONFIG_ENTRY
|
||||
|
||||
await hass.config_entries.async_set_disabled_by(config_entry1.entry_id, None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entry1 = registry.async_get(entry1.id)
|
||||
assert not entry1.disabled
|
||||
|
||||
Reference in New Issue
Block a user