Compare commits

..

40 Commits

Author SHA1 Message Date
Paulus Schoutsen 447901c223 Bumped version to 2021.8.0b4 2021-07-29 23:35:09 -07:00
Paulus Schoutsen 9dcd3f6626 Add energy attributes to Fronius (#53741)
* Add energy attributes to Fronius

* Add solar

* Add power

* Only add last reset for total meter entities

* Only add last reset for total solar entities

* Create different entity descriptions per key

* only return the entity description for energy total

* Use correct key

* Meter devices keep it real

* keys start with energy_real

* Also device key starts with

* Lint
2021-07-29 23:35:03 -07:00
Jan Bouwhuis d34bd8ad1e Fix Xiaomi-miio switch platform setup (#53739) 2021-07-29 23:35:02 -07:00
Jan Bouwhuis 83e4e4f769 Fix Xiaomi humidifier name migration (#53738) 2021-07-29 23:35:01 -07:00
Michael 128dc07fa5 Apply left suggestions #53596 for TP-Link (#53737) 2021-07-29 23:35:00 -07:00
Brandon Rothweiler d2dfdd81ad Only allow one Mazda vehicle status request at a time (#53736) 2021-07-29 23:35:00 -07:00
Michael 0442827b9e Fix exception handling in DataUpdateCoordinator in TP-Link (#53734)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2021-07-29 23:34:59 -07:00
Alexei Chetroi 37c3062874 Bump up ZHA dependencies (#53732) 2021-07-29 23:34:58 -07:00
Robert Svensson bfacff5d78 Add energy device class to deCONZ consumption sensors (#53731) 2021-07-29 23:34:57 -07:00
Michael d54621e778 Extract smartthings switch energy attributes into sensors (#53719) 2021-07-29 23:34:57 -07:00
Ryan Johnson 716c3f69ca Bump pyatv to 0.8.2 (#53659) 2021-07-29 23:34:56 -07:00
Paulus Schoutsen 630a1fb36c Bumped version to 2021.8.0b3 2021-07-29 14:27:55 -07:00
Paulus Schoutsen 54eeebfd20 Revert "Allow uploading large snapshots (#53528)" (#53729)
This reverts commit cdce14d63d.
2021-07-29 14:27:47 -07:00
Franck Nijhof a671a0ccac Fix effect selector of light.turn_on service (#53726) 2021-07-29 14:27:46 -07:00
Martin Hjelmare 8cf0182f2f Fix zwave_js current and voltage meter sensor device class (#53723) 2021-07-29 14:27:45 -07:00
Franck Nijhof f1400b03bb Fix DSMR reconnecting loop without timeout (#53722) 2021-07-29 14:27:45 -07:00
Paulus Schoutsen 6dc00d3d87 Bumped version to 2021.8.0b2 2021-07-29 12:35:50 -07:00
Franck Nijhof bf6133534d Fix SolarEdge statistics; missing device_class (#53720) 2021-07-29 12:35:25 -07:00
Paulus Schoutsen b1758e1fcc Bump frontend to 20210729.0 (#53717) 2021-07-29 12:35:24 -07:00
Martin Hjelmare cc0aa32f3e Fix zwave_js meter sensor state class (#53716) 2021-07-29 12:35:23 -07:00
Franck Nijhof dc2494f0a0 Add state class support to DSMR Reader (#53715) 2021-07-29 12:35:23 -07:00
Simone Chemelli 4b2a1ec694 Add last reset to Shelly's energy entities (#53710) 2021-07-29 12:35:22 -07:00
Michael b2187022c4 Set state class measurement also for Total Energy for AVM Fritz!Smarthome devices (#53707) 2021-07-29 12:35:21 -07:00
Martin Hjelmare c6f588fc08 Revert "Add Automate Pulse Hub v2 support (#39501)" (#53704) 2021-07-29 12:35:20 -07:00
Daniel Hjelseth Høyer 462e3a3d0d Integration. Add device class, last_reset, state_class (#53698)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2021-07-29 12:35:20 -07:00
Daniel Hjelseth Høyer aa179a1ad9 Energy round (#53696)
* Energy. Round cost

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Energy. Round cost

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Update homeassistant/components/energy/sensor.py

Co-authored-by: Franck Nijhof <git@frenck.dev>

Co-authored-by: Franck Nijhof <git@frenck.dev>
2021-07-29 12:35:19 -07:00
Daniel Hjelseth Høyer 1117158bd0 Surepetcare, bug fix (#53695) 2021-07-29 12:35:18 -07:00
Andrew55529 6ced0153df Fix problem with telegram_bot (#53690) 2021-07-29 12:35:17 -07:00
Gerard 7f314e17de Bump bimmer_connected to 0.7.16 to fix parking light issue (#53687) 2021-07-29 12:35:17 -07:00
Robert Svensson 9ad29ae75c Only disable a device if all associated config entries are disabled (#53681) 2021-07-29 12:35:16 -07:00
Maciej Bieniek 43a89dc452 Fix last_reset_topic config replaces state_topic for sensor platform (#53677) 2021-07-29 12:35:15 -07:00
Allen Porter 268f0dd62f Bump nest to version 0.3.5 (#53672)
Fix runtime type assertions, Fixes Issue #53652
2021-07-29 12:35:15 -07:00
Eric Severance d7768f13c1 pyWeMo version bump (0.6.6) (#53671) 2021-07-29 12:35:14 -07:00
J. Nick Koston db8aa4658a Skip each ssdp listener that fails to bind (#53670) 2021-07-29 12:35:13 -07:00
Raman Gupta d19d487b21 Add energy support for zwave_js meter CC entities (#53665)
* Add energy support for zwave_js meter CC entities

* shrink

* comments

* comments

* comments

* Move attributes

* Add tests

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-07-29 12:35:13 -07:00
Aaron Bach 2aeecba64c Fix unhandled exception with Guardian paired sensor coordinators (#53663) 2021-07-29 12:35:12 -07:00
Diogo Gomes b3367d8b3f Prosegur code quality improvements (#53647) 2021-07-29 12:35:11 -07:00
Raman Gupta 7e6856ace8 Add enabled attribute to zwave_js discovery model (#53645)
* Add attribute to zwave_js discovery model

* Fix binary sensor entity enabled logic

* Add tests
2021-07-29 12:35:10 -07:00
Michael b5f0c2cef4 Move TP-Link power and energy switch attributes to sensors (#53596)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2021-07-29 12:35:10 -07:00
Stephen Beechen 2ffc779f3d Allow uploading large snapshots (#53528)
Co-authored-by: Pascal Vizeli <pascal.vizeli@syshack.ch>
2021-07-29 12:35:09 -07:00
97 changed files with 2203 additions and 1838 deletions
-6
View File
@@ -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
-1
View File
@@ -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
-93
View File
@@ -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_{}"
-147
View File
@@ -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,
)
-89
View File
@@ -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."""
+3
View File
@@ -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
+14 -60
View File
@@ -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
)
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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,
+88 -29
View File
@@ -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"
+6 -6
View File
@@ -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]
+26 -3
View File
@@ -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
+1 -5
View File
@@ -296,11 +296,7 @@ turn_on:
name: Effect
description: Light effect.
selector:
select:
options:
- colorloop
- random
- white
text:
turn_off:
name: Turn off
+6 -9
View File
@@ -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
+1 -1
View File
@@ -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],
+1 -1
View File
@@ -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],
},
+3
View File
@@ -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"
+2 -2
View File
@@ -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:
+17 -4
View File
@@ -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:
+1 -1
View File
@@ -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
+208 -55
View File
@@ -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."""
+1 -11
View File
@@ -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."""
+34 -23
View File
@@ -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",
+4 -12
View File
@@ -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 -24
View File
@@ -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
+14 -2
View File
@@ -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.
+117 -15
View File
@@ -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
+17 -16
View File
@@ -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()
+21
View File
@@ -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]
+156
View File
@@ -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],
}
+46 -155
View File
@@ -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()
+1 -1
View File
@@ -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}",
+4 -12
View File
@@ -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}",
+1 -1
View File
@@ -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:
+50 -7
View File
@@ -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 = {
+82 -16
View File
@@ -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):
+1 -1
View File
@@ -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)
-1
View File
@@ -28,7 +28,6 @@ FLOWS = [
"atag",
"august",
"aurora",
"automate",
"awair",
"axis",
"azure_devops",
+11
View File
@@ -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)
+1 -1
View File
@@ -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
+6 -9
View File
@@ -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
+6 -9
View File
@@ -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
View File
@@ -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"}
+2 -1
View File
@@ -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")
+1 -1
View File
@@ -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):
+106 -10
View File
@@ -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):
+1 -1
View File
@@ -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,
+3 -23
View File
@@ -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(
-2
View File
@@ -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={
+45 -1
View File
@@ -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
+1 -7
View File
@@ -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):
+62
View File
@@ -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
+72
View File
@@ -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",
}
+184 -18
View File
@@ -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)
+1 -1
View File
@@ -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,
+11 -1
View File
@@ -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)
+18
View File
@@ -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
+88 -10
View File
@@ -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()
)
+42
View File
@@ -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