Compare commits

...

67 Commits

Author SHA1 Message Date
Paulus Schoutsen
f4c3bdad7d Merge pull request #48896 from home-assistant/rc 2021-04-08 15:35:17 -07:00
Paulus Schoutsen
3bf693e352 Bumped version to 2021.4.1 2021-04-08 21:35:53 +00:00
Bram Kragten
7051cc04bd Update frontend to 20210407.2 (#48888) 2021-04-08 21:35:37 +00:00
Franck Nijhof
d9c1c391bc Fix optional data payload in Prowl messaging service (#48868) 2021-04-08 21:35:35 +00:00
Franck Nijhof
02cd2619bb Fix possibly missing changed_by in Verisure Alarm (#48867) 2021-04-08 21:35:35 +00:00
starkillerOG
f791142c75 Fix motion_blinds gateway signal strength sensor (#48866)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-04-08 21:35:34 +00:00
Bram Kragten
1c939fc9be Correct wrong x in frontend manifest (#48865) 2021-04-08 21:35:33 +00:00
Philip Allgaier
0ad4736349 Bump speedtest-cli to 2.1.3 (#48861) 2021-04-08 21:35:32 +00:00
Erik Montnemery
3f0d63c1ab Validate supported_color_modes for MQTT JSON light (#48836) 2021-04-08 21:35:32 +00:00
Martin Hjelmare
f39afa60ae Fix mysensor cover closed state (#48833) 2021-04-08 21:35:31 +00:00
Erik Montnemery
cf11d9a2df Replace redacted stream recorder credentials with '****' (#48832) 2021-04-08 21:35:30 +00:00
Niccolo Zapponi
dd2a73b363 Fix iCloud extra attributes (#48815) 2021-04-08 21:35:29 +00:00
Johan Nenzén
99ef870908 Add missing super call in Verisure Camera entity (#48812) 2021-04-08 21:35:29 +00:00
Raman Gupta
8d738cff41 Check all endpoints for zwave_js.climate fan mode and operating state (#48800)
* Check all endpoints for zwave_js.climate fan mode and operating state

* fix test
2021-04-08 21:35:28 +00:00
Franck Nijhof
8bdcdfb8e6 Merge pull request #48782 from home-assistant/rc 2021-04-07 19:07:10 +02:00
Franck Nijhof
341531146d Bumped version to 2021.4.0 2021-04-07 18:31:01 +02:00
Bram Kragten
49178d6865 Update frontend to 20210407.1 (#48778) 2021-04-07 18:17:15 +02:00
Erik Montnemery
b4636f17fb Reject nan, inf from generic_thermostat sensor (#48771) 2021-04-07 18:17:11 +02:00
Franck Nijhof
0fb4f31bde Bumped version to 2021.4.0b6 2021-04-07 12:43:04 +02:00
Bram Kragten
b382de96c6 Update frontend to 20210407.0 (#48765) 2021-04-07 12:40:14 +02:00
Erik Montnemery
c9f8861303 Fix whitespace error in cast (#48763) 2021-04-07 12:40:08 +02:00
Erik Montnemery
32511409a9 Remove login details before logging SQL errors (#48758) 2021-04-07 12:40:04 +02:00
Daniel Hjelseth Høyer
e366961ddb Met.no - only update data if coordinates changed (#48756) 2021-04-07 12:40:00 +02:00
J. Nick Koston
bfb8141f55 Solve cast delaying startup when discovered devices are slow to setup (#48755)
* Solve cast delaying startup when devices are slow to setup

* Update homeassistant/components/cast/media_player.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2021-04-07 12:39:57 +02:00
Joakim Sørensen
537d6412dd Add custom integrations to analytics (#48753) 2021-04-07 12:39:54 +02:00
Stefan Agner
a093cd8ac2 Use microsecond precision for datetime values on MariaDB/MySQL (#48749)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-07 12:39:51 +02:00
Franck Nijhof
322458ee49 Rename hassio config entry title to Supervisor (#48748) 2021-04-07 12:39:48 +02:00
Joakim Sørensen
b573fb49b7 Generate a seperate UUID for the analytics integration (#48742) 2021-04-07 12:39:45 +02:00
Franck Nijhof
15e00b8d18 Do not activate Met.no without setting a Home coordinates (#48741) 2021-04-07 12:39:41 +02:00
Paulus Schoutsen
2db60a3c56 Bumped version to 2021.4.0b5 2021-04-06 19:12:33 +00:00
Paulus Schoutsen
ed90e22421 Updated frontend to 20210406.0 (#48734) 2021-04-06 19:12:28 +00:00
Paulus Schoutsen
d61780dbac Allow reloading top-level template entities (#48733) 2021-04-06 19:12:27 +00:00
Justin Paupore
315e910bfe Fix infinite recursion in LazyState (#48719)
If LazyState cannot parse the attributes of its row as JSON, it prints
a message to the logger. Unfortunately, it passes `self` as a format
argument to that message, which causes its `__repr__` method to be
called, which then tries to retrieve `self.attributes` in order to
display them. This leads to an infinite recursion and a crash of the
entire core.

To fix, send the database row to be printed in the log message, rather
than the LazyState object that wraps around it.
2021-04-06 19:12:26 +00:00
Erik Montnemery
a7523777ba Flag brightness support for MQTT RGB lights (#48718) 2021-04-06 19:12:25 +00:00
Erik Montnemery
7ae65832eb Bump pychromecast to 9.1.2 (#48714) 2021-04-06 19:12:24 +00:00
Erik Montnemery
0df9a8ec38 Improve warnings on undefined template errors (#48713) 2021-04-06 19:12:23 +00:00
J. Nick Koston
5f2a666e76 Abort discovery for unsupported doorbird accessories (#48710) 2021-04-06 19:12:23 +00:00
Paulus Schoutsen
26b9017905 Fix verisure deadlock (#48691) 2021-04-06 19:12:22 +00:00
Raman Gupta
bdd68cd413 Bump zwave_js dependency to 0.23.1 (#48682) 2021-04-06 19:12:21 +00:00
Alexei Chetroi
c512ab7ec9 Implement Ignore list for poll control configuration on Ikea devices (#48667)
Co-authored-by: Hmmbob <33529490+hmmbob@users.noreply.github.com>
2021-04-06 19:12:21 +00:00
mburget
edf41e8425 Fix Raspi GPIO binary_sensor produces unreliable responses (#48170)
* Fix for issue #10498 Raspi GPIO binary_sensor produces unreliable responses ("Doorbell Scenario")

Changes overtaken from PR#31788 which was somehow never finished

* Fix for issue #10498 Raspi GPIO binary_sensor produces unreliable response. Changes taken over from PR31788 which was somehow never finished

* Remove unused code (pylint warning)
2021-04-06 19:12:20 +00:00
Paulus Schoutsen
1850b92b36 Bumped version to 2021.4.0b4 2021-04-04 00:36:15 +00:00
J. Nick Koston
7b1ea46653 Prevent config entry retry from blocking startup (#48660)
- If there are two integrations doing long retries async_block_till_done() will never be done
2021-04-04 00:36:07 +00:00
Álvaro Fernández Rojas
a8cd6228cf Fix AEMET town timestamp format (#48647)
Datetime should be converted to ISO format.

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2021-04-04 00:36:07 +00:00
J. Nick Koston
9eb4397837 Only listen for zeroconf when the esphome device cannot connect (#48645) 2021-04-04 00:36:06 +00:00
J. Nick Koston
311f624adc Bump aiodiscover to 1.3.3 for dhcp (#48644)
fixes #48615
2021-04-04 00:36:05 +00:00
Paulus Schoutsen
dcb43b474f Bumped version to 2021.4.0b3 2021-04-03 00:05:20 +00:00
Bram Kragten
396a8a3a10 Updated frontend to 20210402.1 (#48639) 2021-04-02 23:57:55 +00:00
Paulus Schoutsen
2a1f6d7e8f Support modern config for the trigger based template entity (#48635) 2021-04-02 23:57:55 +00:00
Paulus Schoutsen
da31328150 Fix trigger template entities without a unique ID (#48631) 2021-04-02 23:57:54 +00:00
Shay Levy
cec80210a3 Bump aioshelly to 0.6.2 (#48620) 2021-04-02 23:57:53 +00:00
Paulus Schoutsen
74357d9760 Bumped version to 2021.4.0b2 2021-04-01 23:33:37 +00:00
Erik Montnemery
231a55d416 Include blueprint input in automation trace (#48575) 2021-04-01 23:33:04 +00:00
Erik Montnemery
e760c23f37 Include script script_execution in script and automation traces (#48576) 2021-04-01 23:32:47 +00:00
Paulus Schoutsen
39f68de5fa Bumped version to 2021.4.0b1 2021-04-01 23:23:47 +00:00
Robert Svensson
68b189cf9f Increase time out for http requests done in Axis integration (#48610) 2021-04-01 23:23:31 +00:00
Bram Kragten
8d0941ba65 Update frontend to 20210402.0 (#48609) 2021-04-01 23:23:29 +00:00
Paulus Schoutsen
d1a48c7c5c Clean up mobile app (#48607)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-04-01 23:23:28 +00:00
Franck Nijhof
f0f8b79be0 Fix websocket search for related (#48603)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-01 23:23:26 +00:00
Franck Nijhof
c2d17a72b7 Allow templatable service target to support scripts (#48600) 2021-04-01 23:23:25 +00:00
Franck Nijhof
f0bd3c577f Upgrade numpy to 1.20.2 (#48597) 2021-04-01 23:23:24 +00:00
Erik Montnemery
947ac514b9 Return config entry details for 1-step config flows (#48585) 2021-04-01 23:23:22 +00:00
epenet
5df90b32fc Cleanup orphan devices in onewire integration (#48581)
* Cleanup orphan devices (https://github.com/home-assistant/core/issues/47438)

* Refactor unit testing

* Filter device entries for this config entry

* Update logging

* Cleanup check
2021-04-01 23:23:20 +00:00
Robert Svensson
f08e7dccdf Don't care about DPI entries when looking for clients to be restored from UniFi (#48579)
* DPI switches shouldnt be restored, they're not part of clients to be restored

* Only care about Block and POE switch entries
2021-04-01 23:23:20 +00:00
Aaron Bach
3982849275 Fix incorrect constant import in Ambient PWS (#48574) 2021-04-01 23:23:17 +00:00
Joakim Sørensen
07827ca55d Remove analytics from default_config (#48566) 2021-04-01 23:23:16 +00:00
youknowjack0
16da181692 Fix timer.finish to cancel callback (#48549)
Timer.finish doesn't cancel the callback, which can lead to incorrect early cancellation of the timer if it is subsequently restarted. 

Bug reported here: https://community.home-assistant.io/t/timer-component-timer-stops-before-time-is-up/96038
2021-04-01 23:23:15 +00:00
116 changed files with 3085 additions and 1537 deletions

View File

@@ -283,7 +283,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
temperature_feeling = None
town_id = None
town_name = None
town_timestamp = dt_util.as_utc(elaborated)
town_timestamp = dt_util.as_utc(elaborated).isoformat()
wind_bearing = None
wind_max_speed = None
wind_speed = None

View File

@@ -1,6 +1,5 @@
"""Support for Ambient Weather Station sensors."""
from homeassistant.components.binary_sensor import DOMAIN as SENSOR
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity
from homeassistant.const import ATTR_NAME
from homeassistant.core import callback

View File

@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import async_call_later, async_track_time_interval
from .analytics import Analytics
from .const import ATTR_HUUID, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
from .const import ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
async def async_setup(hass: HomeAssistant, _):
@@ -44,10 +44,9 @@ async def websocket_analytics(
) -> None:
"""Return analytics preferences."""
analytics: Analytics = hass.data[DOMAIN]
huuid = await hass.helpers.instance_id.async_get()
connection.send_result(
msg["id"],
{ATTR_PREFERENCES: analytics.preferences, ATTR_HUUID: huuid},
{ATTR_PREFERENCES: analytics.preferences},
)

View File

@@ -1,5 +1,6 @@
"""Analytics helper class for the analytics integration."""
import asyncio
import uuid
import aiohttp
import async_timeout
@@ -7,7 +8,7 @@ import async_timeout
from homeassistant.components import hassio
from homeassistant.components.api import ATTR_INSTALLATION_TYPE
from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.storage import Store
@@ -22,9 +23,9 @@ from .const import (
ATTR_AUTO_UPDATE,
ATTR_AUTOMATION_COUNT,
ATTR_BASE,
ATTR_CUSTOM_INTEGRATIONS,
ATTR_DIAGNOSTICS,
ATTR_HEALTHY,
ATTR_HUUID,
ATTR_INTEGRATION_COUNT,
ATTR_INTEGRATIONS,
ATTR_ONBOARDED,
@@ -37,6 +38,7 @@ from .const import (
ATTR_SUPPORTED,
ATTR_USAGE,
ATTR_USER_COUNT,
ATTR_UUID,
ATTR_VERSION,
LOGGER,
PREFERENCE_SCHEMA,
@@ -52,7 +54,7 @@ class Analytics:
"""Initialize the Analytics class."""
self.hass: HomeAssistant = hass
self.session = async_get_clientsession(hass)
self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False}
self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False, ATTR_UUID: None}
self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
@property
@@ -71,6 +73,11 @@ class Analytics:
"""Return bool if the user has made a choice."""
return self._data[ATTR_ONBOARDED]
@property
def uuid(self) -> bool:
"""Return the uuid for the analytics integration."""
return self._data[ATTR_UUID]
@property
def supervisor(self) -> bool:
"""Return bool if a supervisor is present."""
@@ -81,6 +88,7 @@ class Analytics:
stored = await self._store.async_load()
if stored:
self._data = stored
if self.supervisor:
supervisor_info = hassio.get_supervisor_info(self.hass)
if not self.onboarded:
@@ -99,6 +107,7 @@ class Analytics:
preferences = PREFERENCE_SCHEMA(preferences)
self._data[ATTR_PREFERENCES].update(preferences)
self._data[ATTR_ONBOARDED] = True
await self._store.async_save(self._data)
if self.supervisor:
@@ -114,16 +123,19 @@ class Analytics:
LOGGER.debug("Nothing to submit")
return
huuid = await self.hass.helpers.instance_id.async_get()
if self._data.get(ATTR_UUID) is None:
self._data[ATTR_UUID] = uuid.uuid4().hex
await self._store.async_save(self._data)
if self.supervisor:
supervisor_info = hassio.get_supervisor_info(self.hass)
system_info = await async_get_system_info(self.hass)
integrations = []
custom_integrations = []
addons = []
payload: dict = {
ATTR_HUUID: huuid,
ATTR_UUID: self.uuid,
ATTR_VERSION: HA_VERSION,
ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE],
}
@@ -152,7 +164,16 @@ class Analytics:
if isinstance(integration, BaseException):
raise integration
if integration.disabled or not integration.is_built_in:
if integration.disabled:
continue
if not integration.is_built_in:
custom_integrations.append(
{
ATTR_DOMAIN: integration.domain,
ATTR_VERSION: integration.version,
}
)
continue
integrations.append(integration.domain)
@@ -176,6 +197,7 @@ class Analytics:
if self.preferences.get(ATTR_USAGE, False):
payload[ATTR_INTEGRATIONS] = integrations
payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations
if supervisor_info is not None:
payload[ATTR_ADDONS] = addons

View File

@@ -18,9 +18,9 @@ ATTR_ADDONS = "addons"
ATTR_AUTO_UPDATE = "auto_update"
ATTR_AUTOMATION_COUNT = "automation_count"
ATTR_BASE = "base"
ATTR_CUSTOM_INTEGRATIONS = "custom_integrations"
ATTR_DIAGNOSTICS = "diagnostics"
ATTR_HEALTHY = "healthy"
ATTR_HUUID = "huuid"
ATTR_INSTALLATION_TYPE = "installation_type"
ATTR_INTEGRATION_COUNT = "integration_count"
ATTR_INTEGRATIONS = "integrations"
@@ -34,6 +34,7 @@ ATTR_SUPERVISOR = "supervisor"
ATTR_SUPPORTED = "supported"
ATTR_USAGE = "usage"
ATTR_USER_COUNT = "user_count"
ATTR_UUID = "uuid"
ATTR_VERSION = "version"

View File

@@ -57,6 +57,7 @@ from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.trace import (
TraceElement,
script_execution_set,
trace_append_element,
trace_get,
trace_path,
@@ -272,6 +273,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
variables,
trigger_variables,
raw_config,
blueprint_inputs,
):
"""Initialize an automation entity."""
self._id = automation_id
@@ -289,6 +291,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._variables: ScriptVariables = variables
self._trigger_variables: ScriptVariables = trigger_variables
self._raw_config = raw_config
self._blueprint_inputs = blueprint_inputs
@property
def name(self):
@@ -436,7 +439,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
trigger_context = Context(parent_id=parent_id)
with trace_automation(
self.hass, self.unique_id, self._raw_config, trigger_context
self.hass,
self.unique_id,
self._raw_config,
self._blueprint_inputs,
trigger_context,
) as automation_trace:
if self._variables:
try:
@@ -471,6 +478,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
"Conditions not met, aborting automation. Condition summary: %s",
trace_get(clear=False),
)
script_execution_set("failed_conditions")
return
self.async_set_context(trigger_context)
@@ -601,10 +609,12 @@ async def _async_process_config(
]
for list_no, config_block in enumerate(conf):
raw_blueprint_inputs = None
raw_config = None
if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore
blueprints_used = True
blueprint_inputs = config_block
raw_blueprint_inputs = blueprint_inputs.config_with_inputs
try:
raw_config = blueprint_inputs.async_substitute()
@@ -673,6 +683,7 @@ async def _async_process_config(
variables,
config_block.get(CONF_TRIGGER_VARIABLES),
raw_config,
raw_blueprint_inputs,
)
entities.append(entity)

View File

@@ -18,11 +18,12 @@ class AutomationTrace(ActionTrace):
self,
item_id: str,
config: dict[str, Any],
blueprint_inputs: dict[str, Any],
context: Context,
):
"""Container for automation trace."""
key = ("automation", item_id)
super().__init__(key, config, context)
super().__init__(key, config, blueprint_inputs, context)
self._trigger_description: str | None = None
def set_trigger_description(self, trigger: str) -> None:
@@ -37,9 +38,9 @@ class AutomationTrace(ActionTrace):
@contextmanager
def trace_automation(hass, automation_id, config, context):
def trace_automation(hass, automation_id, config, blueprint_inputs, context):
"""Trace action execution of automation with automation_id."""
trace = AutomationTrace(automation_id, config, context)
trace = AutomationTrace(automation_id, config, blueprint_inputs, context)
async_store_trace(hass, trace)
try:

View File

@@ -304,7 +304,7 @@ async def get_device(hass, host, port, username, password):
)
try:
with async_timeout.timeout(15):
with async_timeout.timeout(30):
await device.vapix.initialize()
return device

View File

@@ -3,7 +3,7 @@
"name": "Axis",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/axis",
"requirements": ["axis==43"],
"requirements": ["axis==44"],
"dhcp": [
{ "hostname": "axis-00408c*", "macaddress": "00408C*" },
{ "hostname": "axis-accc8e*", "macaddress": "ACCC8E*" },

View File

@@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==9.1.1"],
"requirements": ["pychromecast==9.1.2"],
"after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"],
"zeroconf": ["_googlecast._tcp.local."],
"codeowners": ["@emontnemery"]

View File

@@ -1,6 +1,7 @@
"""Provide functionality to interact with Cast devices on the network."""
from __future__ import annotations
import asyncio
from contextlib import suppress
from datetime import timedelta
import functools as ft
@@ -185,7 +186,9 @@ class CastDevice(MediaPlayerEntity):
)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop)
self.async_set_cast_info(self._cast_info)
self.hass.async_create_task(
# asyncio.create_task is used to avoid delaying startup wrapup if the device
# is discovered already during startup but then fails to respond
asyncio.create_task(
async_create_catching_coro(self.async_connect_to_chromecast())
)

View File

@@ -97,6 +97,17 @@ class ConfigManagerEntryResourceReloadView(HomeAssistantView):
return self.json({"require_restart": not result})
def _prepare_config_flow_result_json(result, prepare_result_json):
"""Convert result to JSON."""
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return prepare_result_json(result)
data = result.copy()
data["result"] = entry_json(result["result"])
data.pop("data")
return data
class ConfigManagerFlowIndexView(FlowManagerIndexView):
"""View to create config flows."""
@@ -118,13 +129,7 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
def _prepare_result_json(self, result):
"""Convert result to JSON."""
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return super()._prepare_result_json(result)
data = result.copy()
data["result"] = data["result"].entry_id
data.pop("data")
return data
return _prepare_config_flow_result_json(result, super()._prepare_result_json)
class ConfigManagerFlowResourceView(FlowManagerResourceView):
@@ -151,13 +156,7 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView):
def _prepare_result_json(self, result):
"""Convert result to JSON."""
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return super()._prepare_result_json(result)
data = result.copy()
data["result"] = entry_json(result["result"])
data.pop("data")
return data
return _prepare_config_flow_result_json(result, super()._prepare_result_json)
class ConfigManagerAvailableFlowView(HomeAssistantView):

View File

@@ -3,7 +3,6 @@
"name": "Default Config",
"documentation": "https://www.home-assistant.io/integrations/default_config",
"dependencies": [
"analytics",
"automation",
"cloud",
"counter",

View File

@@ -3,7 +3,7 @@
"name": "DHCP Discovery",
"documentation": "https://www.home-assistant.io/integrations/dhcp",
"requirements": [
"scapy==2.4.4", "aiodiscover==1.3.2"
"scapy==2.4.4", "aiodiscover==1.3.3"
],
"codeowners": [
"@bdraco"

View File

@@ -1,11 +1,10 @@
"""Support for DoorBird devices."""
import asyncio
import logging
import urllib
from urllib.error import HTTPError
from aiohttp import web
from doorbirdpy import DoorBird
import requests
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
@@ -130,8 +129,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
device = DoorBird(device_ip, username, password)
try:
status, info = await hass.async_add_executor_job(_init_doorbird_device, device)
except urllib.error.HTTPError as err:
if err.code == HTTP_UNAUTHORIZED:
except requests.exceptions.HTTPError as err:
if err.response.status_code == HTTP_UNAUTHORIZED:
_LOGGER.error(
"Authorization rejected by DoorBird for %s@%s", username, device_ip
)
@@ -202,7 +201,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
async def _async_register_events(hass, doorstation):
try:
await hass.async_add_executor_job(doorstation.register_events, hass)
except HTTPError:
except requests.exceptions.HTTPError:
hass.components.persistent_notification.async_create(
"Doorbird configuration failed. Please verify that API "
"Operator permission is enabled for the Doorbird user. "

View File

@@ -1,9 +1,9 @@
"""Config flow for DoorBird integration."""
from ipaddress import ip_address
import logging
import urllib
from doorbirdpy import DoorBird
import requests
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
@@ -34,17 +34,18 @@ def _schema_with_defaults(host=None, name=None):
)
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
def _check_device(device):
"""Verify we can connect to the device and return the status."""
return device.ready(), device.info()
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect."""
device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
try:
status = await hass.async_add_executor_job(device.ready)
info = await hass.async_add_executor_job(device.info)
except urllib.error.HTTPError as err:
if err.code == HTTP_UNAUTHORIZED:
status, info = await hass.async_add_executor_job(_check_device, device)
except requests.exceptions.HTTPError as err:
if err.response.status_code == HTTP_UNAUTHORIZED:
raise InvalidAuth from err
raise CannotConnect from err
except OSError as err:
@@ -59,6 +60,19 @@ async def validate_input(hass: core.HomeAssistant, data):
return {"title": data[CONF_HOST], "mac_addr": mac_addr}
async def async_verify_supported_device(hass, host):
"""Verify the doorbell state endpoint returns a 401."""
device = DoorBird(host, "", "")
try:
await hass.async_add_executor_job(device.doorbell_state)
except requests.exceptions.HTTPError as err:
if err.response.status_code == HTTP_UNAUTHORIZED:
return True
except OSError:
return False
return False
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for DoorBird."""
@@ -85,17 +99,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_zeroconf(self, discovery_info):
"""Prepare configuration for a discovered doorbird device."""
macaddress = discovery_info["properties"]["macaddress"]
host = discovery_info[CONF_HOST]
if macaddress[:6] != DOORBIRD_OUI:
return self.async_abort(reason="not_doorbird_device")
if is_link_local(ip_address(discovery_info[CONF_HOST])):
if is_link_local(ip_address(host)):
return self.async_abort(reason="link_local_address")
if not await async_verify_supported_device(self.hass, host):
return self.async_abort(reason="not_doorbird_device")
await self.async_set_unique_id(macaddress)
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info[CONF_HOST]}
)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
chop_ending = "._axis-video._tcp.local."
friendly_hostname = discovery_info["name"]
@@ -104,11 +119,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.context["title_placeholders"] = {
CONF_NAME: friendly_hostname,
CONF_HOST: discovery_info[CONF_HOST],
CONF_HOST: host,
}
self.discovery_schema = _schema_with_defaults(
host=discovery_info[CONF_HOST], name=friendly_hostname
)
self.discovery_schema = _schema_with_defaults(host=host, name=friendly_hostname)
return await self.async_step_user()

View File

@@ -239,6 +239,8 @@ class ReconnectLogic(RecordUpdateListener):
# Flag to check if the device is connected
self._connected = True
self._connected_lock = asyncio.Lock()
self._zc_lock = asyncio.Lock()
self._zc_listening = False
# Event the different strategies use for issuing a reconnect attempt.
self._reconnect_event = asyncio.Event()
# The task containing the infinite reconnect loop while running
@@ -270,6 +272,7 @@ class ReconnectLogic(RecordUpdateListener):
self._entry_data.disconnect_callbacks = []
self._entry_data.available = False
self._entry_data.async_update_device_state(self._hass)
await self._start_zc_listen()
# Reset tries
async with self._tries_lock:
@@ -315,6 +318,7 @@ class ReconnectLogic(RecordUpdateListener):
self._host,
error,
)
await self._start_zc_listen()
# Schedule re-connect in event loop in order not to delay HA
# startup. First connect is scheduled in tracked tasks.
async with self._wait_task_lock:
@@ -332,6 +336,7 @@ class ReconnectLogic(RecordUpdateListener):
self._tries = 0
async with self._connected_lock:
self._connected = True
await self._stop_zc_listen()
self._hass.async_create_task(self._on_login())
async def _reconnect_once(self):
@@ -375,9 +380,6 @@ class ReconnectLogic(RecordUpdateListener):
# Create reconnection loop outside of HA's tracked tasks in order
# not to delay startup.
self._loop_task = self._hass.loop.create_task(self._reconnect_loop())
# Listen for mDNS records so we can reconnect directly if a received mDNS record
# indicates the node is up again
await self._hass.async_add_executor_job(self._zc.add_listener, self, None)
async with self._connected_lock:
self._connected = False
@@ -388,11 +390,31 @@ class ReconnectLogic(RecordUpdateListener):
if self._loop_task is not None:
self._loop_task.cancel()
self._loop_task = None
await self._hass.async_add_executor_job(self._zc.remove_listener, self)
async with self._wait_task_lock:
if self._wait_task is not None:
self._wait_task.cancel()
self._wait_task = None
await self._stop_zc_listen()
async def _start_zc_listen(self):
"""Listen for mDNS records.
This listener allows us to schedule a reconnect as soon as a
received mDNS record indicates the node is up again.
"""
async with self._zc_lock:
if not self._zc_listening:
await self._hass.async_add_executor_job(
self._zc.add_listener, self, None
)
self._zc_listening = True
async def _stop_zc_listen(self):
"""Stop listening for zeroconf updates."""
async with self._zc_lock:
if self._zc_listening:
await self._hass.async_add_executor_job(self._zc.remove_listener, self)
self._zc_listening = False
@callback
def stop_callback(self):

View File

@@ -62,7 +62,7 @@ MANIFEST_JSON = {
"screenshots": [
{
"src": "/static/images/screenshots/screenshot-1.png",
"sizes": "413×792",
"sizes": "413x792",
"type": "image/png",
}
],

View File

@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20210331.0"
"home-assistant-frontend==20210407.2"
],
"dependencies": [
"api",

View File

@@ -1,6 +1,7 @@
"""Adds support for generic thermostat units."""
import asyncio
import logging
import math
import voluptuous as vol
@@ -419,7 +420,10 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
def _async_update_temp(self, state):
"""Update thermostat with latest state from sensor."""
try:
self._cur_temp = float(state.state)
cur_temp = float(state.state)
if math.isnan(cur_temp) or math.isinf(cur_temp):
raise ValueError(f"Sensor has illegal state {state.state}")
self._cur_temp = cur_temp
except ValueError as ex:
_LOGGER.error("Unable to update from sensor: %s", ex)

View File

@@ -19,4 +19,4 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# We only need one Hass.io config entry
await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=DOMAIN.title(), data={})
return self.async_create_entry(title="Supervisor", data={})

View File

@@ -715,7 +715,7 @@ class LazyState(State):
self._attributes = json.loads(self._row.attributes)
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting row to state: %s", self)
_LOGGER.exception("Error converting row to state: %s", self._row)
self._attributes = {}
return self._attributes

View File

@@ -501,6 +501,6 @@ class IcloudDevice:
return self._location
@property
def exta_state_attributes(self) -> dict[str, any]:
def extra_state_attributes(self) -> dict[str, any]:
"""Return the attributes."""
return self._attrs

View File

@@ -110,7 +110,7 @@ class IcloudTrackerEntity(TrackerEntity):
@property
def extra_state_attributes(self) -> dict[str, any]:
"""Return the device state attributes."""
return self._device.state_attributes
return self._device.extra_state_attributes
@property
def device_info(self) -> dict[str, any]:

View File

@@ -93,7 +93,7 @@ class IcloudDeviceBatterySensor(SensorEntity):
@property
def extra_state_attributes(self) -> dict[str, any]:
"""Return default attributes for the iCloud device entity."""
return self._device.state_attributes
return self._device.extra_state_attributes
@property
def device_info(self) -> dict[str, any]:

View File

@@ -3,6 +3,6 @@
"name": "IQVIA",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iqvia",
"requirements": ["numpy==1.19.2", "pyiqvia==0.3.1"],
"requirements": ["numpy==1.20.2", "pyiqvia==0.3.1"],
"codeowners": ["@bachya"]
}

View File

@@ -73,6 +73,20 @@ VALID_COLOR_MODES = {
COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF}
COLOR_MODES_COLOR = {COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_XY}
def valid_supported_color_modes(color_modes):
"""Validate the given color modes."""
color_modes = set(color_modes)
if (
not color_modes
or COLOR_MODE_UNKNOWN in color_modes
or (COLOR_MODE_BRIGHTNESS in color_modes and len(color_modes) > 1)
or (COLOR_MODE_ONOFF in color_modes and len(color_modes) > 1)
):
raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}")
return color_modes
# Float that represents transition time in seconds to make change.
ATTR_TRANSITION = "transition"

View File

@@ -19,7 +19,12 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from homeassistant.util.distance import convert as convert_distance
import homeassistant.util.dt as dt_util
from .const import CONF_TRACK_HOME, DOMAIN
from .const import (
CONF_TRACK_HOME,
DEFAULT_HOME_LATITUDE,
DEFAULT_HOME_LONGITUDE,
DOMAIN,
)
URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete"
@@ -35,6 +40,20 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool:
async def async_setup_entry(hass, config_entry):
"""Set up Met as config entry."""
# Don't setup if tracking home location and latitude or longitude isn't set.
# Also, filters out our onboarding default location.
if config_entry.data.get(CONF_TRACK_HOME, False) and (
(not hass.config.latitude and not hass.config.longitude)
or (
hass.config.latitude == DEFAULT_HOME_LATITUDE
and hass.config.longitude == DEFAULT_HOME_LONGITUDE
)
):
_LOGGER.warning(
"Skip setting up met.no integration; No Home location has been set"
)
return False
coordinator = MetDataUpdateCoordinator(hass, config_entry)
await coordinator.async_config_entry_first_refresh()
@@ -68,7 +87,7 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator):
self.weather = MetWeatherData(
hass, config_entry.data, hass.config.units.is_metric
)
self.weather.init_data()
self.weather.set_coordinates()
update_interval = timedelta(minutes=randrange(55, 65))
@@ -88,8 +107,8 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator):
async def _async_update_weather_data(_event=None):
"""Update weather data."""
self.weather.init_data()
await self.async_refresh()
if self.weather.set_coordinates():
await self.async_refresh()
self._unsub_track_home = self.hass.bus.async_listen(
EVENT_CORE_CONFIG_UPDATE, _async_update_weather_data
@@ -114,9 +133,10 @@ class MetWeatherData:
self.current_weather_data = {}
self.daily_forecast = None
self.hourly_forecast = None
self._coordinates = None
def init_data(self):
"""Weather data inialization - get the coordinates."""
def set_coordinates(self):
"""Weather data inialization - set the coordinates."""
if self._config.get(CONF_TRACK_HOME, False):
latitude = self.hass.config.latitude
longitude = self.hass.config.longitude
@@ -136,10 +156,14 @@ class MetWeatherData:
"lon": str(longitude),
"msl": str(elevation),
}
if coordinates == self._coordinates:
return False
self._coordinates = coordinates
self._weather_data = metno.MetWeatherData(
coordinates, async_get_clientsession(self.hass), api_url=URL
)
return True
async def fetch_data(self):
"""Fetch data from API - (current weather and forecast)."""

View File

@@ -10,7 +10,13 @@ from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, C
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME
from .const import (
CONF_TRACK_HOME,
DEFAULT_HOME_LATITUDE,
DEFAULT_HOME_LONGITUDE,
DOMAIN,
HOME_LOCATION_NAME,
)
@callback
@@ -81,6 +87,14 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_onboarding(self, data=None):
"""Handle a flow initialized by onboarding."""
# Don't create entry if latitude or longitude isn't set.
# Also, filters out our onboarding default location.
if (not self.hass.config.latitude and not self.hass.config.longitude) or (
self.hass.config.latitude == DEFAULT_HOME_LATITUDE
and self.hass.config.longitude == DEFAULT_HOME_LONGITUDE
):
return self.async_abort(reason="no_home")
return self.async_create_entry(
title=HOME_LOCATION_NAME, data={CONF_TRACK_HOME: True}
)

View File

@@ -34,6 +34,9 @@ HOME_LOCATION_NAME = "Home"
CONF_TRACK_HOME = "track_home"
DEFAULT_HOME_LATITUDE = 52.3731339
DEFAULT_HOME_LONGITUDE = 4.8903147
ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_{HOME_LOCATION_NAME}"
CONDITIONS_MAP = {

View File

@@ -12,6 +12,11 @@
}
}
},
"error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }
"error": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"abort": {
"no_home": "No home coordinates are set in the Home Assistant configuration"
}
}
}

View File

@@ -1,6 +1,4 @@
"""Binary sensor platform for mobile_app."""
from functools import partial
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, STATE_ON
from homeassistant.core import callback
@@ -48,7 +46,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities)
@callback
def handle_sensor_registration(webhook_id, data):
def handle_sensor_registration(data):
if data[CONF_WEBHOOK_ID] != webhook_id:
return
@@ -66,7 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_dispatcher_connect(
hass,
f"{DOMAIN}_{ENTITY_TYPE}_register",
partial(handle_sensor_registration, webhook_id),
handle_sensor_registration,
)

View File

@@ -34,13 +34,14 @@ class MobileAppEntity(RestoreEntity):
self._registration = entry.data
self._unique_id = config[CONF_UNIQUE_ID]
self._entity_type = config[ATTR_SENSOR_TYPE]
self.unsub_dispatcher = None
self._name = config[CONF_NAME]
async def async_added_to_hass(self):
"""Register callbacks."""
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update
)
)
state = await self.async_get_last_state()
@@ -49,11 +50,6 @@ class MobileAppEntity(RestoreEntity):
self.async_restore_last_state(state)
async def async_will_remove_from_hass(self):
"""Disconnect dispatcher listener when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
@callback
def async_restore_last_state(self, last_state):
"""Restore previous state."""

View File

@@ -84,17 +84,16 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO):
async def async_get_service(hass, config, discovery_info=None):
"""Get the mobile_app notification service."""
session = async_get_clientsession(hass)
service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(session)
service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(hass)
return service
class MobileAppNotificationService(BaseNotificationService):
"""Implement the notification service for mobile_app."""
def __init__(self, session):
def __init__(self, hass):
"""Initialize the service."""
self._session = session
self._hass = hass
@property
def targets(self):
@@ -141,7 +140,9 @@ class MobileAppNotificationService(BaseNotificationService):
try:
with async_timeout.timeout(10):
response = await self._session.post(push_url, json=data)
response = await async_get_clientsession(self._hass).post(
push_url, json=data
)
result = await response.json()
if response.status in [HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED]:

View File

@@ -1,6 +1,4 @@
"""Sensor platform for mobile_app."""
from functools import partial
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID
from homeassistant.core import callback
@@ -50,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities)
@callback
def handle_sensor_registration(webhook_id, data):
def handle_sensor_registration(data):
if data[CONF_WEBHOOK_ID] != webhook_id:
return
@@ -68,7 +66,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_dispatcher_connect(
hass,
f"{DOMAIN}_{ENTITY_TYPE}_register",
partial(handle_sensor_registration, webhook_id),
handle_sensor_registration,
)

View File

@@ -472,6 +472,7 @@ async def webhook_update_sensor_states(hass, config_entry, data):
device_name = config_entry.data[ATTR_DEVICE_NAME]
resp = {}
for sensor in data:
entity_type = sensor[ATTR_SENSOR_TYPE]
@@ -495,8 +496,6 @@ async def webhook_update_sensor_states(hass, config_entry, data):
}
continue
entry = {CONF_WEBHOOK_ID: config_entry.data[CONF_WEBHOOK_ID]}
try:
sensor = sensor_schema_full(sensor)
except vol.Invalid as err:
@@ -513,9 +512,8 @@ async def webhook_update_sensor_states(hass, config_entry, data):
}
continue
new_state = {**entry, **sensor}
async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state)
sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, sensor)
resp[unique_id] = {"success": True}

View File

@@ -183,10 +183,14 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity):
if self.coordinator.data is None:
return False
if not self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]:
return False
gateway_available = self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]
if self._device_type == TYPE_GATEWAY:
return gateway_available
return self.coordinator.data[self._device.mac][ATTR_AVAILABLE]
return (
gateway_available
and self.coordinator.data[self._device.mac][ATTR_AVAILABLE]
)
@property
def unit_of_measurement(self):

View File

@@ -35,6 +35,7 @@ from homeassistant.components.light import (
SUPPORT_WHITE_VALUE,
VALID_COLOR_MODES,
LightEntity,
valid_supported_color_modes,
)
from homeassistant.const import (
CONF_BRIGHTNESS,
@@ -130,7 +131,10 @@ PLATFORM_SCHEMA_JSON = vol.All(
vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All(
cv.ensure_list, [vol.In(VALID_COLOR_MODES)], vol.Unique()
cv.ensure_list,
[vol.In(VALID_COLOR_MODES)],
vol.Unique(),
valid_supported_color_modes,
),
vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean,
vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean,
@@ -197,7 +201,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
self._supported_features |= config[CONF_BRIGHTNESS] and SUPPORT_BRIGHTNESS
self._supported_features |= config[CONF_COLOR_TEMP] and SUPPORT_COLOR_TEMP
self._supported_features |= config[CONF_HS] and SUPPORT_COLOR
self._supported_features |= config[CONF_RGB] and SUPPORT_COLOR
self._supported_features |= config[CONF_RGB] and (
SUPPORT_COLOR | SUPPORT_BRIGHTNESS
)
self._supported_features |= config[CONF_WHITE_VALUE] and SUPPORT_WHITE_VALUE
self._supported_features |= config[CONF_XY] and SUPPORT_COLOR

View File

@@ -417,7 +417,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
and self._templates[CONF_GREEN_TEMPLATE] is not None
and self._templates[CONF_BLUE_TEMPLATE] is not None
):
features = features | SUPPORT_COLOR
features = features | SUPPORT_COLOR | SUPPORT_BRIGHTNESS
if self._config.get(CONF_EFFECT_LIST) is not None:
features = features | SUPPORT_EFFECT
if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None:

View File

@@ -70,12 +70,12 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
else:
amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0
if amount == 0:
return CoverState.CLOSED
if v_up and not v_down and not v_stop:
return CoverState.OPENING
if not v_up and v_down and not v_stop:
return CoverState.CLOSING
if not v_up and not v_down and v_stop and amount == 0:
return CoverState.CLOSED
return CoverState.OPEN
@property

View File

@@ -1,13 +1,17 @@
"""The 1-Wire component."""
import asyncio
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN, PLATFORMS
from .onewirehub import CannotConnect, OneWireHub
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Set up 1-Wire integrations."""
@@ -26,10 +30,43 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry):
hass.data[DOMAIN][config_entry.unique_id] = onewirehub
for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
async def cleanup_registry() -> None:
# Get registries
device_registry, entity_registry = await asyncio.gather(
hass.helpers.device_registry.async_get_registry(),
hass.helpers.entity_registry.async_get_registry(),
)
# Generate list of all device entries
registry_devices = [
entry.id
for entry in dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
]
# Remove devices that don't belong to any entity
for device_id in registry_devices:
if not er.async_entries_for_device(
entity_registry, device_id, include_disabled_entities=True
):
_LOGGER.debug(
"Removing device `%s` because it does not have any entities",
device_id,
)
device_registry.async_remove_device(device_id)
async def start_platforms() -> None:
"""Start platforms and cleanup devices."""
# wait until all required platforms are ready
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_setup(config_entry, platform)
for platform in PLATFORMS
]
)
await cleanup_registry()
hass.async_create_task(start_platforms())
return True

View File

@@ -2,6 +2,6 @@
"domain": "opencv",
"name": "OpenCV",
"documentation": "https://www.home-assistant.io/integrations/opencv",
"requirements": ["numpy==1.19.2", "opencv-python-headless==4.3.0.36"],
"requirements": ["numpy==1.20.2", "opencv-python-headless==4.3.0.36"],
"codeowners": []
}

View File

@@ -48,7 +48,7 @@ class ProwlNotificationService(BaseNotificationService):
"description": message,
"priority": data["priority"] if data and "priority" in data else 0,
}
if data.get("url"):
if data and data.get("url"):
payload["url"] = data["url"]
_LOGGER.debug("Attempting call Prowl service at %s", url)

View File

@@ -363,6 +363,20 @@ def _apply_update(engine, new_version, old_version):
if engine.dialect.name == "mysql":
_modify_columns(engine, "events", ["event_data LONGTEXT"])
_modify_columns(engine, "states", ["attributes LONGTEXT"])
elif new_version == 13:
if engine.dialect.name == "mysql":
_modify_columns(
engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"]
)
_modify_columns(
engine,
"states",
[
"last_changed DATETIME(6)",
"last_updated DATETIME(6)",
"created DATETIME(6)",
],
)
else:
raise ValueError(f"No schema migration defined for version {new_version}")

View File

@@ -26,7 +26,7 @@ import homeassistant.util.dt as dt_util
# pylint: disable=invalid-name
Base = declarative_base()
SCHEMA_VERSION = 12
SCHEMA_VERSION = 13
_LOGGER = logging.getLogger(__name__)
@@ -39,6 +39,10 @@ TABLE_SCHEMA_CHANGES = "schema_changes"
ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES]
DATETIME_TYPE = DateTime(timezone=True).with_variant(
mysql.DATETIME(timezone=True, fsp=6), "mysql"
)
class Events(Base): # type: ignore
"""Event history data."""
@@ -52,8 +56,8 @@ class Events(Base): # type: ignore
event_type = Column(String(32))
event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql"))
origin = Column(String(32))
time_fired = Column(DateTime(timezone=True), index=True)
created = Column(DateTime(timezone=True), default=dt_util.utcnow)
time_fired = Column(DATETIME_TYPE, index=True)
created = Column(DATETIME_TYPE, default=dt_util.utcnow)
context_id = Column(String(36), index=True)
context_user_id = Column(String(36), index=True)
context_parent_id = Column(String(36), index=True)
@@ -123,9 +127,9 @@ class States(Base): # type: ignore
event_id = Column(
Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True
)
last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow)
last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow, index=True)
created = Column(DateTime(timezone=True), default=dt_util.utcnow)
last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow)
last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True)
created = Column(DATETIME_TYPE, default=dt_util.utcnow)
old_state_id = Column(
Integer, ForeignKey("states.state_id", ondelete="NO ACTION"), index=True
)

View File

@@ -1,4 +1,7 @@
"""Support for binary sensor using RPi GPIO."""
import asyncio
import voluptuous as vol
from homeassistant.components import rpi_gpio
@@ -52,6 +55,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class RPiGPIOBinarySensor(BinarySensorEntity):
"""Represent a binary sensor that uses Raspberry Pi GPIO."""
async def async_read_gpio(self):
"""Read state from GPIO."""
await asyncio.sleep(float(self._bouncetime) / 1000)
self._state = await self.hass.async_add_executor_job(
rpi_gpio.read_input, self._port
)
self.async_write_ha_state()
def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
"""Initialize the RPi binary sensor."""
self._name = name or DEVICE_DEFAULT_NAME
@@ -63,12 +74,11 @@ class RPiGPIOBinarySensor(BinarySensorEntity):
rpi_gpio.setup_input(self._port, self._pull_mode)
def read_gpio(port):
"""Read state from GPIO."""
self._state = rpi_gpio.read_input(self._port)
self.schedule_update_ha_state()
def edge_detected(port):
"""Edge detection handler."""
self.hass.add_job(self.async_read_gpio)
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
rpi_gpio.edge_detect(self._port, edge_detected, self._bouncetime)
@property
def should_poll(self):

View File

@@ -19,7 +19,7 @@ class ScriptTrace(ActionTrace):
):
"""Container for automation trace."""
key = ("script", item_id)
super().__init__(key, config, context)
super().__init__(key, config, None, context)
@contextmanager

View File

@@ -19,7 +19,6 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "search/related",
@@ -38,6 +37,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
vol.Required("item_id"): str,
}
)
@callback
def websocket_search_related(hass, connection, msg):
"""Handle search."""
searcher = Searcher(

View File

@@ -3,7 +3,7 @@
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==0.6.1"],
"requirements": ["aioshelly==0.6.2"],
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
"codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"]
}

View File

@@ -3,6 +3,8 @@
"name": "Speedtest.net",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/speedtestdotnet",
"requirements": ["speedtest-cli==2.1.2"],
"requirements": [
"speedtest-cli==2.1.3"
],
"codeowners": ["@rohankapoorcom", "@engrbm87"]
}

View File

@@ -2,6 +2,7 @@
import datetime
import decimal
import logging
import re
import sqlalchemy
from sqlalchemy.orm import scoped_session, sessionmaker
@@ -18,6 +19,13 @@ CONF_COLUMN_NAME = "column"
CONF_QUERIES = "queries"
CONF_QUERY = "query"
DB_URL_RE = re.compile("//.*:.*@")
def redact_credentials(data):
"""Redact credentials from string data."""
return DB_URL_RE.sub("//****:****@", data)
def validate_sql_select(value):
"""Validate that value is a SQL SELECT query."""
@@ -47,6 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if not db_url:
db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE))
sess = None
try:
engine = sqlalchemy.create_engine(db_url)
sessmaker = scoped_session(sessionmaker(bind=engine))
@@ -56,10 +65,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
sess.execute("SELECT 1;")
except sqlalchemy.exc.SQLAlchemyError as err:
_LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err)
_LOGGER.error(
"Couldn't connect using %s DB_URL: %s",
redact_credentials(db_url),
redact_credentials(str(err)),
)
return
finally:
sess.close()
if sess:
sess.close()
queries = []
@@ -147,7 +161,11 @@ class SQLSensor(SensorEntity):
value = str(value)
self._attributes[key] = value
except sqlalchemy.exc.SQLAlchemyError as err:
_LOGGER.error("Error executing query %s: %s", self._query, err)
_LOGGER.error(
"Error executing query %s: %s",
self._query,
redact_credentials(str(err)),
)
return
finally:
sess.close()

View File

@@ -39,7 +39,12 @@ from .hls import async_setup_hls
_LOGGER = logging.getLogger(__name__)
STREAM_SOURCE_RE = re.compile("//(.*):(.*)@")
STREAM_SOURCE_RE = re.compile("//.*:.*@")
def redact_credentials(data):
"""Redact credentials from string data."""
return STREAM_SOURCE_RE.sub("//****:****@", data)
def create_stream(hass, stream_source, options=None):
@@ -176,9 +181,7 @@ class Stream:
target=self._run_worker,
)
self._thread.start()
_LOGGER.info(
"Started stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source))
)
_LOGGER.info("Started stream: %s", redact_credentials(str(self.source)))
def update_source(self, new_source):
"""Restart the stream with a new stream source."""
@@ -244,9 +247,7 @@ class Stream:
self._thread_quit.set()
self._thread.join()
self._thread = None
_LOGGER.info(
"Stopped stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source))
)
_LOGGER.info("Stopped stream: %s", redact_credentials(str(self.source)))
async def async_record(self, video_path, duration=30, lookback=5):
"""Make a .mp4 recording from a provided stream."""

View File

@@ -5,7 +5,7 @@ import logging
import av
from . import STREAM_SOURCE_RE
from . import redact_credentials
from .const import (
AUDIO_CODECS,
MAX_MISSING_DTS,
@@ -128,9 +128,7 @@ def stream_worker(source, options, segment_buffer, quit_event):
try:
container = av.open(source, options=options, timeout=STREAM_TIMEOUT)
except av.AVError:
_LOGGER.error(
"Error opening stream %s", STREAM_SOURCE_RE.sub("//", str(source))
)
_LOGGER.error("Error opening stream %s", redact_credentials(str(source)))
return
try:
video_stream = container.streams.video[0]

View File

@@ -1,68 +1,131 @@
"""The template component."""
import logging
from typing import Optional
from __future__ import annotations
from homeassistant.const import CONF_SENSORS, EVENT_HOMEASSISTANT_START
from homeassistant.core import CoreState, callback
import asyncio
import logging
from typing import Callable
from homeassistant import config as conf_util
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD
from homeassistant.core import CoreState, Event, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
discovery,
trigger as trigger_helper,
update_coordinator,
)
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.reload import async_reload_integration_platforms
from homeassistant.loader import async_get_integration
from .const import CONF_TRIGGER, DOMAIN, PLATFORMS
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Set up the template integration."""
if DOMAIN in config:
for conf in config[DOMAIN]:
coordinator = TriggerUpdateCoordinator(hass, conf)
await coordinator.async_setup(config)
await _process_config(hass, config)
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
async def _reload_config(call: Event) -> None:
"""Reload top-level + platforms."""
try:
unprocessed_conf = await conf_util.async_hass_config_yaml(hass)
except HomeAssistantError as err:
_LOGGER.error(err)
return
conf = await conf_util.async_process_component_config(
hass, unprocessed_conf, await async_get_integration(hass, DOMAIN)
)
if conf is None:
return
await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS)
if DOMAIN in conf:
await _process_config(hass, conf)
hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context)
hass.helpers.service.async_register_admin_service(
DOMAIN, SERVICE_RELOAD, _reload_config
)
return True
async def _process_config(hass, config):
"""Process config."""
coordinators: list[TriggerUpdateCoordinator] | None = hass.data.get(DOMAIN)
# Remove old ones
if coordinators:
for coordinator in coordinators:
coordinator.async_remove()
async def init_coordinator(hass, conf):
coordinator = TriggerUpdateCoordinator(hass, conf)
await coordinator.async_setup(conf)
return coordinator
hass.data[DOMAIN] = await asyncio.gather(
*[init_coordinator(hass, conf) for conf in config[DOMAIN]]
)
class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator):
"""Class to handle incoming data."""
REMOVE_TRIGGER = object()
def __init__(self, hass, config):
"""Instantiate trigger data."""
super().__init__(
hass, logging.getLogger(__name__), name="Trigger Update Coordinator"
)
super().__init__(hass, _LOGGER, name="Trigger Update Coordinator")
self.config = config
self._unsub_trigger = None
self._unsub_start: Callable[[], None] | None = None
self._unsub_trigger: Callable[[], None] | None = None
@property
def unique_id(self) -> Optional[str]:
def unique_id(self) -> str | None:
"""Return unique ID for the entity."""
return self.config.get("unique_id")
@callback
def async_remove(self):
"""Signal that the entities need to remove themselves."""
if self._unsub_start:
self._unsub_start()
if self._unsub_trigger:
self._unsub_trigger()
async def async_setup(self, hass_config):
"""Set up the trigger and create entities."""
if self.hass.state == CoreState.running:
await self._attach_triggers()
else:
self.hass.bus.async_listen_once(
self._unsub_start = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, self._attach_triggers
)
self.hass.async_create_task(
discovery.async_load_platform(
self.hass,
"sensor",
DOMAIN,
{"coordinator": self, "entities": self.config[CONF_SENSORS]},
hass_config,
for platform_domain in (SENSOR_DOMAIN,):
self.hass.async_create_task(
discovery.async_load_platform(
self.hass,
platform_domain,
DOMAIN,
{"coordinator": self, "entities": self.config[platform_domain]},
hass_config,
)
)
)
async def _attach_triggers(self, start_event=None) -> None:
"""Attach the triggers."""
if start_event is not None:
self._unsub_start = None
self._unsub_trigger = await trigger_helper.async_initialize_triggers(
self.hass,
self.config[CONF_TRIGGER],

View File

@@ -1,49 +1,128 @@
"""Template config validator."""
import logging
import voluptuous as vol
from homeassistant.components.sensor import (
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
)
from homeassistant.config import async_log_exception, config_without_domain
from homeassistant.const import CONF_SENSORS, CONF_UNIQUE_ID
from homeassistant.helpers import config_validation as cv
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ENTITY_PICTURE_TEMPLATE,
CONF_FRIENDLY_NAME,
CONF_FRIENDLY_NAME_TEMPLATE,
CONF_ICON,
CONF_ICON_TEMPLATE,
CONF_NAME,
CONF_SENSORS,
CONF_STATE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
)
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.trigger import async_validate_trigger_config
from .const import CONF_TRIGGER, DOMAIN
from .sensor import SENSOR_SCHEMA
from .const import (
CONF_ATTRIBUTE_TEMPLATES,
CONF_ATTRIBUTES,
CONF_AVAILABILITY,
CONF_AVAILABILITY_TEMPLATE,
CONF_PICTURE,
CONF_TRIGGER,
DOMAIN,
)
from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA
CONF_STATE = "state"
LEGACY_SENSOR = {
CONF_ICON_TEMPLATE: CONF_ICON,
CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY,
CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES,
CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME,
CONF_FRIENDLY_NAME: CONF_NAME,
CONF_VALUE_TEMPLATE: CONF_STATE,
}
TRIGGER_ENTITY_SCHEMA = vol.Schema(
SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.template,
vol.Required(CONF_STATE): cv.template,
vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_PICTURE): cv.template,
vol.Optional(CONF_AVAILABILITY): cv.template,
vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
CONFIG_SECTION_SCHEMA = vol.Schema(
{
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA),
vol.Optional(SENSOR_DOMAIN): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(PLATFORM_SENSOR_SCHEMA),
}
)
def _rewrite_legacy_to_modern_trigger_conf(cfg: dict):
"""Rewrite a legacy to a modern trigger-basd conf."""
logging.getLogger(__name__).warning(
"The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors"
)
sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else []
for device_id, entity_cfg in cfg[CONF_SENSORS].items():
entity_cfg = {**entity_cfg}
for from_key, to_key in LEGACY_SENSOR.items():
if from_key not in entity_cfg or to_key in entity_cfg:
continue
val = entity_cfg.pop(from_key)
if isinstance(val, str):
val = template.Template(val)
entity_cfg[to_key] = val
if CONF_NAME not in entity_cfg:
entity_cfg[CONF_NAME] = template.Template(device_id)
sensor.append(entity_cfg)
return {**cfg, "sensor": sensor}
async def async_validate_config(hass, config):
"""Validate config."""
if DOMAIN not in config:
return config
trigger_entity_configs = []
config_sections = []
for cfg in cv.ensure_list(config[DOMAIN]):
try:
cfg = TRIGGER_ENTITY_SCHEMA(cfg)
cfg = CONFIG_SECTION_SCHEMA(cfg)
cfg[CONF_TRIGGER] = await async_validate_trigger_config(
hass, cfg[CONF_TRIGGER]
)
except vol.Invalid as err:
async_log_exception(err, DOMAIN, cfg, hass)
continue
else:
trigger_entity_configs.append(cfg)
if CONF_TRIGGER in cfg and CONF_SENSORS in cfg:
cfg = _rewrite_legacy_to_modern_trigger_conf(cfg)
config_sections.append(cfg)
# Create a copy of the configuration with all config for current
# component removed and add validated config back in.
config = config_without_domain(config, DOMAIN)
config[DOMAIN] = trigger_entity_configs
config[DOMAIN] = config_sections
return config

View File

@@ -20,3 +20,7 @@ PLATFORMS = [
"vacuum",
"weather",
]
CONF_AVAILABILITY = "availability"
CONF_ATTRIBUTES = "attributes"
CONF_PICTURE = "picture"

View File

@@ -5,6 +5,7 @@ import voluptuous as vol
from homeassistant.components.sensor import (
DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
ENTITY_ID_FORMAT,
PLATFORM_SCHEMA,
SensorEntity,
@@ -17,6 +18,7 @@ from homeassistant.const import (
CONF_FRIENDLY_NAME_TEMPLATE,
CONF_ICON_TEMPLATE,
CONF_SENSORS,
CONF_STATE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
@@ -88,7 +90,7 @@ def _async_create_template_tracking_entities(hass, config):
friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE)
unit_of_measurement = device_config.get(CONF_UNIT_OF_MEASUREMENT)
device_class = device_config.get(CONF_DEVICE_CLASS)
attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES]
attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {})
unique_id = device_config.get(CONF_UNIQUE_ID)
sensors.append(
@@ -117,8 +119,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(_async_create_template_tracking_entities(hass, config))
else:
async_add_entities(
TriggerSensorEntity(hass, discovery_info["coordinator"], device_id, config)
for device_id, config in discovery_info["entities"].items()
TriggerSensorEntity(hass, discovery_info["coordinator"], config)
for config in discovery_info["entities"]
)
@@ -201,9 +203,10 @@ class SensorTemplate(TemplateEntity, SensorEntity):
class TriggerSensorEntity(TriggerEntity, SensorEntity):
"""Sensor entity based on trigger data."""
extra_template_keys = (CONF_VALUE_TEMPLATE,)
domain = SENSOR_DOMAIN
extra_template_keys = (CONF_STATE,)
@property
def state(self) -> str | None:
"""Return state of the sensor."""
return self._rendered.get(CONF_VALUE_TEMPLATE)
return self._rendered.get(CONF_STATE)

View File

@@ -6,20 +6,16 @@ from typing import Any
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ENTITY_PICTURE_TEMPLATE,
CONF_FRIENDLY_NAME,
CONF_FRIENDLY_NAME_TEMPLATE,
CONF_ICON_TEMPLATE,
CONF_ICON,
CONF_NAME,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import template, update_coordinator
from homeassistant.helpers.entity import async_generate_entity_id
from . import TriggerUpdateCoordinator
from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE
from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE
class TriggerEntity(update_coordinator.CoordinatorEntity):
@@ -32,23 +28,13 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
self,
hass: HomeAssistant,
coordinator: TriggerUpdateCoordinator,
device_id: str,
config: dict,
):
"""Initialize the entity."""
super().__init__(coordinator)
self.entity_id = async_generate_entity_id(
self.domain + ".{}", device_id, hass=hass
)
self._name = config.get(CONF_FRIENDLY_NAME, device_id)
entity_unique_id = config.get(CONF_UNIQUE_ID)
if entity_unique_id is None and coordinator.unique_id:
entity_unique_id = device_id
if entity_unique_id and coordinator.unique_id:
self._unique_id = f"{coordinator.unique_id}-{entity_unique_id}"
else:
@@ -56,32 +42,33 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
self._config = config
self._to_render = [
itm
for itm in (
CONF_VALUE_TEMPLATE,
CONF_ICON_TEMPLATE,
CONF_ENTITY_PICTURE_TEMPLATE,
CONF_FRIENDLY_NAME_TEMPLATE,
CONF_AVAILABILITY_TEMPLATE,
)
if itm in config
]
self._static_rendered = {}
self._to_render = []
for itm in (
CONF_NAME,
CONF_ICON,
CONF_PICTURE,
CONF_AVAILABILITY,
):
if itm not in config:
continue
if config[itm].is_static:
self._static_rendered[itm] = config[itm].template
else:
self._to_render.append(itm)
if self.extra_template_keys is not None:
self._to_render.extend(self.extra_template_keys)
self._rendered = {}
# We make a copy so our initial render is 'unknown' and not 'unavailable'
self._rendered = dict(self._static_rendered)
@property
def name(self):
"""Name of the entity."""
if (
self._rendered is not None
and (name := self._rendered.get(CONF_FRIENDLY_NAME_TEMPLATE)) is not None
):
return name
return self._name
return self._rendered.get(CONF_NAME)
@property
def unique_id(self):
@@ -101,29 +88,27 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
@property
def icon(self) -> str | None:
"""Return icon."""
return self._rendered is not None and self._rendered.get(CONF_ICON_TEMPLATE)
return self._rendered.get(CONF_ICON)
@property
def entity_picture(self) -> str | None:
"""Return entity picture."""
return self._rendered is not None and self._rendered.get(
CONF_ENTITY_PICTURE_TEMPLATE
)
return self._rendered.get(CONF_PICTURE)
@property
def available(self):
"""Return availability of the entity."""
return (
self._rendered is not None
self._rendered is not self._static_rendered
and
# Check against False so `None` is ok
self._rendered.get(CONF_AVAILABILITY_TEMPLATE) is not False
self._rendered.get(CONF_AVAILABILITY) is not False
)
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return extra attributes."""
return self._rendered.get(CONF_ATTRIBUTE_TEMPLATES)
return self._rendered.get(CONF_ATTRIBUTES)
async def async_added_to_hass(self) -> None:
"""Handle being added to Home Assistant."""
@@ -136,16 +121,16 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
try:
rendered = {}
rendered = dict(self._static_rendered)
for key in self._to_render:
rendered[key] = self._config[key].async_render(
self.coordinator.data["run_variables"], parse_result=False
)
if CONF_ATTRIBUTE_TEMPLATES in self._config:
rendered[CONF_ATTRIBUTE_TEMPLATES] = template.render_complex(
self._config[CONF_ATTRIBUTE_TEMPLATES],
if CONF_ATTRIBUTES in self._config:
rendered[CONF_ATTRIBUTES] = template.render_complex(
self._config[CONF_ATTRIBUTES],
self.coordinator.data["run_variables"],
)
@@ -154,7 +139,7 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error(
"Error rendering %s template for %s: %s", key, self.entity_id, err
)
self._rendered = None
self._rendered = self._static_rendered
self.async_set_context(self.coordinator.data["context"])
self.async_write_ha_state()

View File

@@ -6,7 +6,7 @@
"tensorflow==2.3.0",
"tf-models-official==2.3.0",
"pycocotools==2.0.1",
"numpy==1.19.2",
"numpy==1.20.2",
"pillow==8.1.2"
],
"codeowners": []

View File

@@ -327,7 +327,9 @@ class Timer(RestoreEntity):
if self._state != STATUS_ACTIVE:
return
self._listener = None
if self._listener:
self._listener()
self._listener = None
self._state = STATUS_IDLE
self._end = None
self._remaining = None

View File

@@ -8,6 +8,7 @@ from typing import Any, Deque
from homeassistant.core import Context
from homeassistant.helpers.trace import (
TraceElement,
script_execution_get,
trace_id_get,
trace_id_set,
trace_set_child_id,
@@ -47,14 +48,17 @@ class ActionTrace:
self,
key: tuple[str, str],
config: dict[str, Any],
blueprint_inputs: dict[str, Any],
context: Context,
):
"""Container for script trace."""
self._trace: dict[str, Deque[TraceElement]] | None = None
self._config: dict[str, Any] = config
self._blueprint_inputs: dict[str, Any] = blueprint_inputs
self.context: Context = context
self._error: Exception | None = None
self._state: str = "running"
self._script_execution: str | None = None
self.run_id: str = str(next(self._run_ids))
self._timestamp_finish: dt.datetime | None = None
self._timestamp_start: dt.datetime = dt_util.utcnow()
@@ -75,6 +79,7 @@ class ActionTrace:
"""Set finish time."""
self._timestamp_finish = dt_util.utcnow()
self._state = "stopped"
self._script_execution = script_execution_get()
def as_dict(self) -> dict[str, Any]:
"""Return dictionary version of this ActionTrace."""
@@ -90,6 +95,7 @@ class ActionTrace:
{
"trace": traces,
"config": self._config,
"blueprint_inputs": self._blueprint_inputs,
"context": self.context,
}
)
@@ -109,6 +115,7 @@ class ActionTrace:
"last_step": last_step,
"run_id": self.run_id,
"state": self._state,
"script_execution": self._script_execution,
"timestamp": {
"start": self._timestamp_start,
"finish": self._timestamp_finish,

View File

@@ -2,7 +2,7 @@
"domain": "trend",
"name": "Trend",
"documentation": "https://www.home-assistant.io/integrations/trend",
"requirements": ["numpy==1.19.2"],
"requirements": ["numpy==1.20.2"],
"codeowners": [],
"quality_scale": "internal"
}

View File

@@ -29,6 +29,7 @@ import async_timeout
from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.unifi.switch import BLOCK_SWITCH, POE_SWITCH
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import (
CONF_HOST,
@@ -347,7 +348,10 @@ class UniFiController:
):
if entry.domain == TRACKER_DOMAIN:
mac = entry.unique_id.split("-", 1)[0]
elif entry.domain == SWITCH_DOMAIN:
elif entry.domain == SWITCH_DOMAIN and (
entry.unique_id.startswith(BLOCK_SWITCH)
or entry.unique_id.startswith(POE_SWITCH)
):
mac = entry.unique_id.split("-", 1)[1]
else:
continue

View File

@@ -112,7 +112,7 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity):
self._state = ALARM_STATE_TO_HA.get(
self.coordinator.data["alarm"]["statusType"]
)
self._changed_by = self.coordinator.data["alarm"]["name"]
self._changed_by = self.coordinator.data["alarm"].get("name")
super()._handle_coordinator_update()
async def async_added_to_hass(self) -> None:

View File

@@ -36,7 +36,7 @@ async def async_setup_entry(
assert hass.config.config_dir
async_add_entities(
VerisureSmartcam(hass, coordinator, serial_number, hass.config.config_dir)
VerisureSmartcam(coordinator, serial_number, hass.config.config_dir)
for serial_number in coordinator.data["cameras"]
)
@@ -48,19 +48,18 @@ class VerisureSmartcam(CoordinatorEntity, Camera):
def __init__(
self,
hass: HomeAssistant,
coordinator: VerisureDataUpdateCoordinator,
serial_number: str,
directory_path: str,
):
"""Initialize Verisure File Camera component."""
super().__init__(coordinator)
Camera.__init__(self)
self.serial_number = serial_number
self._directory_path = directory_path
self._image = None
self._image_id = None
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image)
@property
def name(self) -> str:
@@ -126,7 +125,7 @@ class VerisureSmartcam(CoordinatorEntity, Camera):
self._image_id = new_image_id
self._image = new_image_path
def delete_image(self) -> None:
def delete_image(self, _=None) -> None:
"""Delete an old image."""
remove_image = os.path.join(
self._directory_path, "{}{}".format(self._image_id, ".jpg")
@@ -145,3 +144,8 @@ class VerisureSmartcam(CoordinatorEntity, Camera):
LOGGER.debug("Capturing new image from %s", self.serial_number)
except VerisureError as ex:
LOGGER.error("Could not capture image, %s", ex)
async def async_added_to_hass(self) -> None:
"""Entity added to Home Assistant."""
await super().async_added_to_hass()
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image)

View File

@@ -391,6 +391,9 @@ class PollControl(ZigbeeChannel):
CHECKIN_INTERVAL = 55 * 60 * 4 # 55min
CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s
LONG_POLL = 6 * 4 # 6s
_IGNORED_MANUFACTURER_ID = {
4476,
} # IKEA
async def async_configure_channel_specific(self) -> None:
"""Configure channel: set check-in interval."""
@@ -416,7 +419,13 @@ class PollControl(ZigbeeChannel):
async def check_in_response(self, tsn: int) -> None:
"""Respond to checkin command."""
await self.checkin_response(True, self.CHECKIN_FAST_POLL_TIMEOUT, tsn=tsn)
await self.set_long_poll_interval(self.LONG_POLL)
if self._ch_pool.manufacturer_code not in self._IGNORED_MANUFACTURER_ID:
await self.set_long_poll_interval(self.LONG_POLL)
@callback
def skip_manufacturer_id(self, manufacturer_code: int) -> None:
"""Block a specific manufacturer id from changing default polling."""
self._IGNORED_MANUFACTURER_ID.add(manufacturer_code)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id)

View File

@@ -169,11 +169,13 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
THERMOSTAT_MODE_PROPERTY,
CommandClass.THERMOSTAT_FAN_MODE,
add_to_watched_value_ids=True,
check_all_endpoints=True,
)
self._fan_state = self.get_zwave_value(
THERMOSTAT_OPERATING_STATE_PROPERTY,
CommandClass.THERMOSTAT_FAN_STATE,
add_to_watched_value_ids=True,
check_all_endpoints=True,
)
self._set_modes_and_presets()
self._supported_features = 0

View File

@@ -3,7 +3,7 @@
"name": "Z-Wave JS",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
"requirements": ["zwave-js-server-python==0.23.0"],
"requirements": ["zwave-js-server-python==0.23.1"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["http", "websocket_api"]
}

View File

@@ -11,7 +11,8 @@ import weakref
import attr
from homeassistant import data_entry_flow, loader
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.event import Event
@@ -276,14 +277,19 @@ class ConfigEntry:
wait_time,
)
async def setup_again(now: Any) -> None:
async def setup_again(*_: Any) -> None:
"""Run setup again."""
self._async_cancel_retry_setup = None
await self.async_setup(hass, integration=integration, tries=tries)
self._async_cancel_retry_setup = hass.helpers.event.async_call_later(
wait_time, setup_again
)
if hass.state == CoreState.running:
self._async_cancel_retry_setup = hass.helpers.event.async_call_later(
wait_time, setup_again
)
else:
self._async_cancel_retry_setup = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, setup_again
)
return
except Exception: # pylint: disable=broad-except
_LOGGER.exception(

View File

@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 2021
MINOR_VERSION = 4
PATCH_VERSION = "0b0"
PATCH_VERSION = "1"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 8, 0)

View File

@@ -916,7 +916,7 @@ SERVICE_SCHEMA = vol.All(
vol.Optional("data"): vol.All(dict, template_complex),
vol.Optional("data_template"): vol.All(dict, template_complex),
vol.Optional(CONF_ENTITY_ID): comp_entity_ids,
vol.Optional(CONF_TARGET): ENTITY_SERVICE_FIELDS,
vol.Optional(CONF_TARGET): vol.Any(ENTITY_SERVICE_FIELDS, dynamic_template),
}
),
has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE),

View File

@@ -63,6 +63,7 @@ from homeassistant.helpers.dispatcher import (
)
from homeassistant.helpers.event import async_call_later, async_track_template
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.trace import script_execution_set
from homeassistant.helpers.trigger import (
async_initialize_triggers,
async_validate_trigger_config,
@@ -332,15 +333,19 @@ class _ScriptRun:
async def async_run(self) -> None:
"""Run script."""
try:
if self._stop.is_set():
return
self._log("Running %s", self._script.running_description)
for self._step, self._action in enumerate(self._script.sequence):
if self._stop.is_set():
script_execution_set("cancelled")
break
await self._async_step(log_exceptions=False)
else:
script_execution_set("finished")
except _StopScript:
pass
script_execution_set("aborted")
except Exception:
script_execution_set("error")
raise
finally:
self._finish()
@@ -1137,6 +1142,7 @@ class Script:
if self.script_mode == SCRIPT_MODE_SINGLE:
if self._max_exceeded != "SILENT":
self._log("Already running", level=LOGSEVERITY[self._max_exceeded])
script_execution_set("failed_single")
return
if self.script_mode == SCRIPT_MODE_RESTART:
self._log("Restarting")
@@ -1147,6 +1153,7 @@ class Script:
"Maximum number of runs exceeded",
level=LOGSEVERITY[self._max_exceeded],
)
script_execution_set("failed_max_runs")
return
# If this is a top level Script then make a copy of the variables in case they

View File

@@ -204,10 +204,15 @@ def async_prepare_call_from_config(
target = {}
if CONF_TARGET in config:
conf = config.get(CONF_TARGET)
conf = config[CONF_TARGET]
try:
template.attach(hass, conf)
target.update(template.render_complex(conf, variables))
if isinstance(conf, template.Template):
conf.hass = hass
target.update(conf.async_render(variables))
else:
template.attach(hass, conf)
target.update(template.render_complex(conf, variables))
if CONF_ENTITY_ID in target:
target[CONF_ENTITY_ID] = cv.comp_entity_ids(target[CONF_ENTITY_ID])
except TemplateError as ex:

View File

@@ -6,6 +6,7 @@ import asyncio
import base64
import collections.abc
from contextlib import suppress
from contextvars import ContextVar
from datetime import datetime, timedelta
from functools import partial, wraps
import json
@@ -79,6 +80,8 @@ _COLLECTABLE_STATE_ATTRIBUTES = {
ALL_STATES_RATE_LIMIT = timedelta(minutes=1)
DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1)
template_cv: ContextVar[str | None] = ContextVar("template_cv", default=None)
@bind_hass
def attach(hass: HomeAssistant, obj: Any) -> None:
@@ -299,7 +302,7 @@ class Template:
self.template: str = template.strip()
self._compiled_code = None
self._compiled: Template | None = None
self._compiled: jinja2.Template | None = None
self.hass = hass
self.is_static = not is_template_string(template)
self._limited = None
@@ -336,7 +339,7 @@ class Template:
If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine.
"""
if self.is_static:
if self.hass.config.legacy_templates or not parse_result:
if not parse_result or self.hass.config.legacy_templates:
return self.template
return self._parse_result(self.template)
@@ -360,7 +363,7 @@ class Template:
If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine.
"""
if self.is_static:
if self.hass.config.legacy_templates or not parse_result:
if not parse_result or self.hass.config.legacy_templates:
return self.template
return self._parse_result(self.template)
@@ -370,7 +373,7 @@ class Template:
kwargs.update(variables)
try:
render_result = compiled.render(kwargs)
render_result = _render_with_context(self.template, compiled, **kwargs)
except Exception as err:
raise TemplateError(err) from err
@@ -442,7 +445,7 @@ class Template:
def _render_template() -> None:
try:
compiled.render(kwargs)
_render_with_context(self.template, compiled, **kwargs)
except TimeoutError:
pass
finally:
@@ -524,7 +527,9 @@ class Template:
variables["value_json"] = json.loads(value)
try:
return self._compiled.render(variables).strip()
return _render_with_context(
self.template, self._compiled, **variables
).strip()
except jinja2.TemplateError as ex:
if error_value is _SENTINEL:
_LOGGER.error(
@@ -535,7 +540,7 @@ class Template:
)
return value if error_value is _SENTINEL else error_value
def _ensure_compiled(self, limited: bool = False) -> Template:
def _ensure_compiled(self, limited: bool = False) -> jinja2.Template:
"""Bind a template to a specific hass instance."""
self.ensure_valid()
@@ -548,7 +553,7 @@ class Template:
env = self._env
self._compiled = cast(
Template,
jinja2.Template,
jinja2.Template.from_code(env, self._compiled_code, env.globals, None),
)
@@ -1314,12 +1319,59 @@ def urlencode(value):
return urllib_urlencode(value).encode("utf-8")
def _render_with_context(
template_str: str, template: jinja2.Template, **kwargs: Any
) -> str:
"""Store template being rendered in a ContextVar to aid error handling."""
template_cv.set(template_str)
return template.render(**kwargs)
class LoggingUndefined(jinja2.Undefined):
"""Log on undefined variables."""
def _log_message(self):
template = template_cv.get() or ""
_LOGGER.warning(
"Template variable warning: %s when rendering '%s'",
self._undefined_message,
template,
)
def _fail_with_undefined_error(self, *args, **kwargs):
try:
return super()._fail_with_undefined_error(*args, **kwargs)
except self._undefined_exception as ex:
template = template_cv.get() or ""
_LOGGER.error(
"Template variable error: %s when rendering '%s'",
self._undefined_message,
template,
)
raise ex
def __str__(self):
"""Log undefined __str___."""
self._log_message()
return super().__str__()
def __iter__(self):
"""Log undefined __iter___."""
self._log_message()
return super().__iter__()
def __bool__(self):
"""Log undefined __bool___."""
self._log_message()
return super().__bool__()
class TemplateEnvironment(ImmutableSandboxedEnvironment):
"""The Home Assistant template environment."""
def __init__(self, hass, limited=False):
"""Initialise template environment."""
super().__init__(undefined=jinja2.make_logging_undefined(logger=_LOGGER))
super().__init__(undefined=LoggingUndefined)
self.hass = hass
self.template_cache = weakref.WeakValueDictionary()
self.filters["round"] = forgiving_round

View File

@@ -88,6 +88,10 @@ variables_cv: ContextVar[Any | None] = ContextVar("variables_cv", default=None)
trace_id_cv: ContextVar[tuple[str, str] | None] = ContextVar(
"trace_id_cv", default=None
)
# Reason for stopped script execution
script_execution_cv: ContextVar[StopReason | None] = ContextVar(
"script_execution_cv", default=None
)
def trace_id_set(trace_id: tuple[str, str]) -> None:
@@ -172,6 +176,7 @@ def trace_clear() -> None:
trace_stack_cv.set(None)
trace_path_stack_cv.set(None)
variables_cv.set(None)
script_execution_cv.set(StopReason())
def trace_set_child_id(child_key: tuple[str, str], child_run_id: str) -> None:
@@ -187,6 +192,28 @@ def trace_set_result(**kwargs: Any) -> None:
node.set_result(**kwargs)
class StopReason:
"""Mutable container class for script_execution."""
script_execution: str | None = None
def script_execution_set(reason: str) -> None:
"""Set stop reason."""
data = script_execution_cv.get()
if data is None:
return
data.script_execution = reason
def script_execution_get() -> str | None:
"""Return the current trace."""
data = script_execution_cv.get()
if data is None:
return None
return data.script_execution
@contextmanager
def trace_path(suffix: str | list[str]) -> Generator:
"""Go deeper in the config tree.

View File

@@ -1,6 +1,6 @@
PyJWT==1.7.1
PyNaCl==1.3.0
aiodiscover==1.3.2
aiodiscover==1.3.3
aiohttp==3.7.4.post0
aiohttp_cors==0.7.0
astral==1.10.1
@@ -16,7 +16,7 @@ defusedxml==0.6.0
distro==1.5.0
emoji==1.2.0
hass-nabucasa==0.42.0
home-assistant-frontend==20210331.0
home-assistant-frontend==20210407.2
httpx==0.17.1
jinja2>=2.11.3
netdisco==2.8.2

View File

@@ -11,7 +11,7 @@ import traceback
from typing import Any, Awaitable, Callable, Coroutine, cast, overload
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback, is_callback
class HideSensitiveDataFilter(logging.Filter):
@@ -138,6 +138,7 @@ def catch_log_exception(
log_exception(format_err, *args)
wrapper_func = async_wrapper
else:
@wraps(func)
@@ -148,6 +149,9 @@ def catch_log_exception(
except Exception: # pylint: disable=broad-except
log_exception(format_err, *args)
if is_callback(check_func):
wrapper = callback(wrapper)
wrapper_func = wrapper
return wrapper_func

View File

@@ -147,7 +147,7 @@ aioazuredevops==1.3.5
aiobotocore==0.11.1
# homeassistant.components.dhcp
aiodiscover==1.3.2
aiodiscover==1.3.3
# homeassistant.components.dnsip
# homeassistant.components.minecraft_server
@@ -224,7 +224,7 @@ aiopylgtv==0.4.0
aiorecollect==1.0.1
# homeassistant.components.shelly
aioshelly==0.6.1
aioshelly==0.6.2
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -313,7 +313,7 @@ av==8.0.3
# avion==0.10
# homeassistant.components.axis
axis==43
axis==44
# homeassistant.components.azure_event_hub
azure-eventhub==5.1.0
@@ -763,7 +763,7 @@ hole==0.5.1
holidays==0.10.5.2
# homeassistant.components.frontend
home-assistant-frontend==20210331.0
home-assistant-frontend==20210407.2
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -1019,7 +1019,7 @@ numato-gpio==0.10.0
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
numpy==1.19.2
numpy==1.20.2
# homeassistant.components.oasa_telematics
oasatelematics==0.3
@@ -1304,7 +1304,7 @@ pycfdns==1.2.1
pychannels==1.0.0
# homeassistant.components.cast
pychromecast==9.1.1
pychromecast==9.1.2
# homeassistant.components.pocketcasts
pycketcasts==1.0.0
@@ -2108,7 +2108,7 @@ sonarr==0.3.0
speak2mary==1.4.0
# homeassistant.components.speedtestdotnet
speedtest-cli==2.1.2
speedtest-cli==2.1.3
# homeassistant.components.spider
spiderpy==1.4.2
@@ -2402,4 +2402,4 @@ zigpy==0.33.0
zm-py==0.5.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.23.0
zwave-js-server-python==0.23.1

View File

@@ -84,7 +84,7 @@ aioazuredevops==1.3.5
aiobotocore==0.11.1
# homeassistant.components.dhcp
aiodiscover==1.3.2
aiodiscover==1.3.3
# homeassistant.components.dnsip
# homeassistant.components.minecraft_server
@@ -143,7 +143,7 @@ aiopylgtv==0.4.0
aiorecollect==1.0.1
# homeassistant.components.shelly
aioshelly==0.6.1
aioshelly==0.6.2
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -187,7 +187,7 @@ auroranoaa==0.0.2
av==8.0.3
# homeassistant.components.axis
axis==43
axis==44
# homeassistant.components.azure_event_hub
azure-eventhub==5.1.0
@@ -412,7 +412,7 @@ hole==0.5.1
holidays==0.10.5.2
# homeassistant.components.frontend
home-assistant-frontend==20210331.0
home-assistant-frontend==20210407.2
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -531,7 +531,7 @@ numato-gpio==0.10.0
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
numpy==1.19.2
numpy==1.20.2
# homeassistant.components.google
oauth2client==4.0.0
@@ -690,7 +690,7 @@ pybotvac==0.0.20
pycfdns==1.2.1
# homeassistant.components.cast
pychromecast==9.1.1
pychromecast==9.1.2
# homeassistant.components.climacell
pyclimacell==0.14.0
@@ -1095,7 +1095,7 @@ sonarr==0.3.0
speak2mary==1.4.0
# homeassistant.components.speedtestdotnet
speedtest-cli==2.1.2
speedtest-cli==2.1.3
# homeassistant.components.spider
spiderpy==1.4.2
@@ -1251,4 +1251,4 @@ zigpy-znp==0.4.0
zigpy==0.33.0
# homeassistant.components.zwave_js
zwave-js-server-python==0.23.0
zwave-js-server-python==0.23.1

View File

@@ -127,7 +127,7 @@ async def test_aemet_weather_create_sensors(hass):
assert state.state == "Getafe"
state = hass.states.get("sensor.aemet_town_timestamp")
assert state.state == "2021-01-09 11:47:45+00:00"
assert state.state == "2021-01-09T11:47:45+00:00"
state = hass.states.get("sensor.aemet_wind_bearing")
assert state.state == "90.0"

View File

@@ -1,5 +1,5 @@
"""The tests for the analytics ."""
from unittest.mock import AsyncMock, Mock, patch
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
import aiohttp
import pytest
@@ -13,10 +13,12 @@ from homeassistant.components.analytics.const import (
ATTR_STATISTICS,
ATTR_USAGE,
)
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.components.api import ATTR_UUID
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
from homeassistant.loader import IntegrationNotFound
from homeassistant.setup import async_setup_component
MOCK_HUUID = "abcdefg"
MOCK_UUID = "abcdefg"
async def test_no_send(hass, caplog, aioclient_mock):
@@ -26,8 +28,7 @@ async def test_no_send(hass, caplog, aioclient_mock):
with patch(
"homeassistant.components.hassio.is_hassio",
side_effect=Mock(return_value=False),
), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
await analytics.load()
):
assert not analytics.preferences[ATTR_BASE]
await analytics.send_analytics()
@@ -76,9 +77,7 @@ async def test_failed_to_send(hass, caplog, aioclient_mock):
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
assert analytics.preferences[ATTR_BASE]
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
await analytics.send_analytics()
await analytics.send_analytics()
assert "Sending analytics failed with statuscode 400" in caplog.text
@@ -88,9 +87,7 @@ async def test_failed_to_send_raises(hass, caplog, aioclient_mock):
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
assert analytics.preferences[ATTR_BASE]
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
await analytics.send_analytics()
await analytics.send_analytics()
assert "Error sending analytics" in caplog.text
@@ -98,12 +95,15 @@ async def test_send_base(hass, caplog, aioclient_mock):
"""Test send base prefrences are defined."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True})
assert analytics.preferences[ATTR_BASE]
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex:
hex.return_value = MOCK_UUID
await analytics.send_analytics()
assert f"'huuid': '{MOCK_HUUID}'" in caplog.text
assert f"'uuid': '{MOCK_UUID}'" in caplog.text
assert f"'version': '{HA_VERSION}'" in caplog.text
assert "'installation_type':" in caplog.text
assert "'integration_count':" not in caplog.text
@@ -131,10 +131,14 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock):
"homeassistant.components.hassio.is_hassio",
side_effect=Mock(return_value=True),
), patch(
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
):
"uuid.UUID.hex", new_callable=PropertyMock
) as hex:
hex.return_value = MOCK_UUID
await analytics.load()
await analytics.send_analytics()
assert f"'huuid': '{MOCK_HUUID}'" in caplog.text
assert f"'uuid': '{MOCK_UUID}'" in caplog.text
assert f"'version': '{HA_VERSION}'" in caplog.text
assert "'supervisor': {'healthy': True, 'supported': True}}" in caplog.text
assert "'installation_type':" in caplog.text
@@ -147,12 +151,13 @@ async def test_send_usage(hass, caplog, aioclient_mock):
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
assert analytics.preferences[ATTR_BASE]
assert analytics.preferences[ATTR_USAGE]
hass.config.components = ["default_config"]
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
await analytics.send_analytics()
await analytics.send_analytics()
assert "'integrations': ['default_config']" in caplog.text
assert "'integration_count':" not in caplog.text
@@ -195,8 +200,6 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock):
), patch(
"homeassistant.components.hassio.is_hassio",
side_effect=Mock(return_value=True),
), patch(
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
):
await analytics.send_analytics()
assert (
@@ -215,8 +218,7 @@ async def test_send_statistics(hass, caplog, aioclient_mock):
assert analytics.preferences[ATTR_STATISTICS]
hass.config.components = ["default_config"]
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
await analytics.send_analytics()
await analytics.send_analytics()
assert (
"'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0"
in caplog.text
@@ -236,11 +238,11 @@ async def test_send_statistics_one_integration_fails(hass, caplog, aioclient_moc
with patch(
"homeassistant.components.analytics.analytics.async_get_integration",
side_effect=IntegrationNotFound("any"),
), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
):
await analytics.send_analytics()
post_call = aioclient_mock.mock_calls[0]
assert "huuid" in post_call[2]
assert "uuid" in post_call[2]
assert post_call[2]["integration_count"] == 0
@@ -258,7 +260,7 @@ async def test_send_statistics_async_get_integration_unknown_exception(
with pytest.raises(ValueError), patch(
"homeassistant.components.analytics.analytics.async_get_integration",
side_effect=ValueError,
), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
):
await analytics.send_analytics()
@@ -298,9 +300,36 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock):
), patch(
"homeassistant.components.hassio.is_hassio",
side_effect=Mock(return_value=True),
), patch(
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
):
await analytics.send_analytics()
assert "'addon_count': 1" in caplog.text
assert "'integrations':" not in caplog.text
async def test_reusing_uuid(hass, aioclient_mock):
"""Test reusing the stored UUID."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
analytics._data[ATTR_UUID] = "NOT_MOCK_UUID"
await analytics.save_preferences({ATTR_BASE: True})
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex:
# This is not actually called but that in itself prove the test
hex.return_value = MOCK_UUID
await analytics.send_analytics()
assert analytics.uuid == "NOT_MOCK_UUID"
async def test_custom_integrations(hass, aioclient_mock):
"""Test sending custom integrations."""
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
analytics = Analytics(hass)
assert await async_setup_component(hass, "test_package", {"test_package": {}})
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
await analytics.send_analytics()
payload = aioclient_mock.mock_calls[0][2]
assert payload["custom_integrations"][0][ATTR_DOMAIN] == "test_package"

View File

@@ -1,6 +1,4 @@
"""The tests for the analytics ."""
from unittest.mock import patch
from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN
from homeassistant.setup import async_setup_component
@@ -22,11 +20,9 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock):
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "analytics"})
with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"):
response = await ws_client.receive_json()
response = await ws_client.receive_json()
assert response["success"]
assert response["result"]["huuid"] == "abcdef"
await ws_client.send_json(
{"id": 2, "type": "analytics/preferences", "preferences": {"base": True}}
@@ -36,7 +32,5 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock):
assert response["result"]["preferences"]["base"]
await ws_client.send_json({"id": 3, "type": "analytics"})
with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"):
response = await ws_client.receive_json()
response = await ws_client.receive_json()
assert response["result"]["preferences"]["base"]
assert response["result"]["huuid"] == "abcdef"

View File

@@ -320,7 +320,17 @@ async def test_create_account(hass, client):
"title": "Test Entry",
"type": "create_entry",
"version": 1,
"result": entries[0].entry_id,
"result": {
"connection_class": "unknown",
"disabled_by": None,
"domain": "test",
"entry_id": entries[0].entry_id,
"source": "user",
"state": "loaded",
"supports_options": False,
"supports_unload": False,
"title": "Test Entry",
},
"description": None,
"description_placeholders": None,
}

View File

@@ -1,6 +1,8 @@
"""Test the DoorBird config flow."""
from unittest.mock import MagicMock, patch
import urllib
from unittest.mock import MagicMock, Mock, patch
import pytest
import requests
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN
@@ -21,7 +23,9 @@ def _get_mock_doorbirdapi_return_values(ready=None, info=None):
doorbirdapi_mock = MagicMock()
type(doorbirdapi_mock).ready = MagicMock(return_value=ready)
type(doorbirdapi_mock).info = MagicMock(return_value=info)
type(doorbirdapi_mock).doorbell_state = MagicMock(
side_effect=requests.exceptions.HTTPError(response=Mock(status_code=401))
)
return doorbirdapi_mock
@@ -137,17 +141,25 @@ async def test_form_import_with_zeroconf_already_discovered(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
doorbirdapi = _get_mock_doorbirdapi_return_values(
ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"}
)
# Running the zeroconf init will make the unique id
# in progress
zero_conf = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
):
zero_conf = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
await hass.async_block_till_done()
assert zero_conf["type"] == data_entry_flow.RESULT_TYPE_FORM
assert zero_conf["step_id"] == "user"
assert zero_conf["errors"] == {}
@@ -159,9 +171,6 @@ async def test_form_import_with_zeroconf_already_discovered(hass):
CONF_CUSTOM_URL
] = "http://legacy.custom.url/should/only/come/in/from/yaml"
doorbirdapi = _get_mock_doorbirdapi_return_values(
ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"}
)
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
@@ -244,24 +253,29 @@ async def test_form_zeroconf_correct_oui(hass):
await hass.async_add_executor_job(
init_recorder_component, hass
) # force in memory db
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
doorbirdapi = _get_mock_doorbirdapi_return_values(
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
)
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
@@ -288,6 +302,43 @@ async def test_form_zeroconf_correct_oui(hass):
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
"doorbell_state_side_effect",
[
requests.exceptions.HTTPError(response=Mock(status_code=404)),
OSError,
None,
],
)
async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_effect):
"""Test we can setup from zeroconf with the correct OUI source but not a doorstation."""
await hass.async_add_executor_job(
init_recorder_component, hass
) # force in memory db
doorbirdapi = _get_mock_doorbirdapi_return_values(
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
)
type(doorbirdapi).doorbell_state = MagicMock(side_effect=doorbell_state_side_effect)
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "not_doorbird_device"
async def test_form_user_cannot_connect(hass):
"""Test we handle cannot connect error."""
await hass.async_add_executor_job(
@@ -322,10 +373,8 @@ async def test_form_user_invalid_auth(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
mock_urllib_error = urllib.error.HTTPError(
"http://xyz.tld", 401, "login failed", {}, None
)
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_urllib_error)
mock_error = requests.exceptions.HTTPError(response=Mock(status_code=401))
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_error)
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,

View File

@@ -331,9 +331,18 @@ async def test_sensor_bad_value(hass, setup_comp_2):
_setup_sensor(hass, None)
await hass.async_block_till_done()
state = hass.states.get(ENTITY)
assert temp == state.attributes.get("current_temperature")
assert state.attributes.get("current_temperature") == temp
_setup_sensor(hass, "inf")
await hass.async_block_till_done()
state = hass.states.get(ENTITY)
assert state.attributes.get("current_temperature") == temp
_setup_sensor(hass, "nan")
await hass.async_block_till_done()
state = hass.states.get(ENTITY)
assert state.attributes.get("current_temperature") == temp
async def test_sensor_unknown(hass):

View File

@@ -18,7 +18,7 @@ async def test_config_flow(hass):
DOMAIN, context={"source": "system"}
)
assert result["type"] == "create_entry"
assert result["title"] == DOMAIN.title()
assert result["title"] == "Supervisor"
assert result["data"] == {}
await hass.async_block_till_done()

View File

@@ -1,20 +1,24 @@
"""Tests for Met.no."""
from unittest.mock import patch
from homeassistant.components.met.const import DOMAIN
from homeassistant.components.met.const import CONF_TRACK_HOME, DOMAIN
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from tests.common import MockConfigEntry
async def init_integration(hass) -> MockConfigEntry:
async def init_integration(hass, track_home=False) -> MockConfigEntry:
"""Set up the Met integration in Home Assistant."""
entry_data = {
CONF_NAME: "test",
CONF_LATITUDE: 0,
CONF_LONGITUDE: 0,
CONF_ELEVATION: 0,
CONF_LONGITUDE: 1.0,
CONF_ELEVATION: 1.0,
}
if track_home:
entry_data = {CONF_TRACK_HOME: True}
entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
with patch(
"homeassistant.components.met.metno.MetWeatherData.fetching_data",

View File

@@ -4,6 +4,7 @@ from unittest.mock import patch
import pytest
from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
from tests.common import MockConfigEntry
@@ -106,6 +107,25 @@ async def test_onboarding_step(hass):
assert result["data"] == {"track_home": True}
@pytest.mark.parametrize("latitude,longitude", [(52.3731339, 4.8903147), (0.0, 0.0)])
async def test_onboarding_step_abort_no_home(hass, latitude, longitude):
"""Test entry not created when default step fails."""
await async_process_ha_core_config(
hass,
{"latitude": latitude, "longitude": longitude},
)
assert hass.config.latitude == latitude
assert hass.config.longitude == longitude
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "onboarding"}, data={}
)
assert result["type"] == "abort"
assert result["reason"] == "no_home"
async def test_import_step(hass):
"""Test initializing via import step."""
test_data = {

View File

@@ -1,6 +1,15 @@
"""Test the Met integration init."""
from homeassistant.components.met.const import DOMAIN
from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
from homeassistant.components.met.const import (
DEFAULT_HOME_LATITUDE,
DEFAULT_HOME_LONGITUDE,
DOMAIN,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_ERROR,
)
from . import init_integration
@@ -17,3 +26,24 @@ async def test_unload_entry(hass):
assert entry.state == ENTRY_STATE_NOT_LOADED
assert not hass.data.get(DOMAIN)
async def test_fail_default_home_entry(hass, caplog):
"""Test abort setup of default home location."""
await async_process_ha_core_config(
hass,
{"latitude": 52.3731339, "longitude": 4.8903147},
)
assert hass.config.latitude == DEFAULT_HOME_LATITUDE
assert hass.config.longitude == DEFAULT_HOME_LONGITUDE
entry = await init_integration(hass, track_home=True)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state == ENTRY_STATE_SETUP_ERROR
assert (
"Skip setting up met.no integration; No Home location has been set"
in caplog.text
)

View File

@@ -29,6 +29,11 @@ async def test_tracking_home(hass, mock_weather):
assert len(mock_weather.mock_calls) == 8
# Same coordinates again should not trigger any new requests to met.no
await hass.config.async_update(latitude=10, longitude=20)
await hass.async_block_till_done()
assert len(mock_weather.mock_calls) == 8
entry = hass.config_entries.async_entries()[0]
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()

View File

@@ -161,7 +161,13 @@ import pytest
from homeassistant import config as hass_config
from homeassistant.components import light
from homeassistant.const import ATTR_ASSUMED_STATE, SERVICE_RELOAD, STATE_OFF, STATE_ON
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_SUPPORTED_FEATURES,
SERVICE_RELOAD,
STATE_OFF,
STATE_ON,
)
import homeassistant.core as ha
from homeassistant.setup import async_setup_component
@@ -206,6 +212,27 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock):
assert hass.states.get("light.test") is None
async def test_rgb_light(hass, mqtt_mock):
"""Test RGB light flags brightness support."""
assert await async_setup_component(
hass,
light.DOMAIN,
{
light.DOMAIN: {
"platform": "mqtt",
"name": "test",
"command_topic": "test_light_rgb/set",
"rgb_command_topic": "test_light_rgb/rgb/set",
}
},
)
await hass.async_block_till_done()
state = hass.states.get("light.test")
expected_features = light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock):
"""Test if there is no color and brightness if no topic."""
assert await async_setup_component(

View File

@@ -188,6 +188,60 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated):
assert hass.states.get("light.test") is None
@pytest.mark.parametrize(
"supported_color_modes", [["onoff", "rgb"], ["brightness", "rgb"], ["unknown"]]
)
async def test_fail_setup_if_color_modes_invalid(
hass, mqtt_mock, supported_color_modes
):
"""Test if setup fails if supported color modes is invalid."""
config = {
light.DOMAIN: {
"brightness": True,
"color_mode": True,
"command_topic": "test_light_rgb/set",
"name": "test",
"platform": "mqtt",
"schema": "json",
"supported_color_modes": supported_color_modes,
}
}
assert await async_setup_component(
hass,
light.DOMAIN,
config,
)
await hass.async_block_till_done()
assert hass.states.get("light.test") is None
async def test_rgb_light(hass, mqtt_mock):
"""Test RGB light flags brightness support."""
assert await async_setup_component(
hass,
light.DOMAIN,
{
light.DOMAIN: {
"platform": "mqtt",
"schema": "json",
"name": "test",
"command_topic": "test_light_rgb/set",
"rgb": True,
}
},
)
await hass.async_block_till_done()
state = hass.states.get("light.test")
expected_features = (
light.SUPPORT_TRANSITION
| light.SUPPORT_COLOR
| light.SUPPORT_FLASH
| light.SUPPORT_BRIGHTNESS
)
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_mock):
"""Test for no RGB, brightness, color temp, effect, white val or XY."""
assert await async_setup_component(

View File

@@ -141,6 +141,37 @@ async def test_setup_fails(hass, mqtt_mock):
assert hass.states.get("light.test") is None
async def test_rgb_light(hass, mqtt_mock):
"""Test RGB light flags brightness support."""
assert await async_setup_component(
hass,
light.DOMAIN,
{
light.DOMAIN: {
"platform": "mqtt",
"schema": "template",
"name": "test",
"command_topic": "test_light_rgb/set",
"command_on_template": "on",
"command_off_template": "off",
"red_template": '{{ value.split(",")[4].' 'split("-")[0] }}',
"green_template": '{{ value.split(",")[4].' 'split("-")[1] }}',
"blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}',
}
},
)
await hass.async_block_till_done()
state = hass.states.get("light.test")
expected_features = (
light.SUPPORT_TRANSITION
| light.SUPPORT_COLOR
| light.SUPPORT_FLASH
| light.SUPPORT_BRIGHTNESS
)
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
async def test_state_change_via_topic(hass, mqtt_mock):
"""Test state change via topic."""
with assert_setup_component(1, light.DOMAIN):

View File

@@ -2,6 +2,8 @@
from unittest.mock import patch
from pyownet.protocol import ProtocolError
from homeassistant.components.onewire.const import (
CONF_MOUNT_DIR,
CONF_NAMES,
@@ -13,6 +15,8 @@ from homeassistant.components.onewire.const import (
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
from .const import MOCK_OWPROXY_DEVICES
from tests.common import MockConfigEntry
@@ -89,3 +93,35 @@ async def setup_onewire_patched_owserver_integration(hass):
await hass.async_block_till_done()
return config_entry
def setup_owproxy_mock_devices(owproxy, domain, device_ids) -> None:
"""Set up mock for owproxy."""
dir_return_value = []
main_read_side_effect = []
sub_read_side_effect = []
for device_id in device_ids:
mock_device = MOCK_OWPROXY_DEVICES[device_id]
# Setup directory listing
dir_return_value += [f"/{device_id}/"]
# Setup device reads
main_read_side_effect += [device_id[0:2].encode()]
if "inject_reads" in mock_device:
main_read_side_effect += mock_device["inject_reads"]
# Setup sub-device reads
device_sensors = mock_device.get(domain, [])
for expected_sensor in device_sensors:
sub_read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect = (
main_read_side_effect
+ sub_read_side_effect
+ [ProtocolError("Missing injected value")] * 20
)
owproxy.return_value.dir.return_value = dir_return_value
owproxy.return_value.read.side_effect = read_side_effect

View File

@@ -1,11 +1,10 @@
"""Tests for 1-Wire devices connected on OWServer."""
from unittest.mock import patch
"""Constants for 1-Wire integration."""
from pi1wire import InvalidCRCException, UnsupportResponseException
from pyownet.protocol import Error as ProtocolError
import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.onewire.const import DOMAIN, PLATFORMS, PRESSURE_CBAR
from homeassistant.components.onewire.const import DOMAIN, PRESSURE_CBAR
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
@@ -24,13 +23,8 @@ from homeassistant.const import (
TEMP_CELSIUS,
VOLT,
)
from homeassistant.setup import async_setup_component
from . import setup_onewire_patched_owserver_integration
from tests.common import mock_device_registry, mock_registry
MOCK_DEVICE_SENSORS = {
MOCK_OWPROXY_DEVICES = {
"00.111111111111": {
"inject_reads": [
b"", # read device type
@@ -186,7 +180,42 @@ MOCK_DEVICE_SENSORS = {
"model": "DS2409",
"name": "1F.111111111111",
},
SENSOR_DOMAIN: [],
"branches": {
"aux": {},
"main": {
"1D.111111111111": {
"inject_reads": [
b"DS2423", # read device type
],
"device_info": {
"identifiers": {(DOMAIN, "1D.111111111111")},
"manufacturer": "Maxim Integrated",
"model": "DS2423",
"name": "1D.111111111111",
},
SENSOR_DOMAIN: [
{
"entity_id": "sensor.1d_111111111111_counter_a",
"device_file": "/1F.111111111111/main/1D.111111111111/counter.A",
"unique_id": "/1D.111111111111/counter.A",
"injected_value": b" 251123",
"result": "251123",
"unit": "count",
"class": None,
},
{
"entity_id": "sensor.1d_111111111111_counter_b",
"device_file": "/1F.111111111111/main/1D.111111111111/counter.B",
"unique_id": "/1D.111111111111/counter.B",
"injected_value": b" 248125",
"result": "248125",
"unit": "count",
"class": None,
},
],
},
},
},
},
"22.111111111111": {
"inject_reads": [
@@ -748,65 +777,106 @@ MOCK_DEVICE_SENSORS = {
},
}
@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys())
@pytest.mark.parametrize("platform", PLATFORMS)
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform):
"""Test for 1-Wire device.
As they would be on a clean setup: all binary-sensors and switches disabled.
"""
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
device_registry = mock_device_registry(hass)
mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
device_family = device_id[0:2]
dir_return_value = [f"/{device_id}/"]
read_side_effect = [device_family.encode()]
if "inject_reads" in mock_device_sensor:
read_side_effect += mock_device_sensor["inject_reads"]
expected_sensors = mock_device_sensor.get(platform, [])
for expected_sensor in expected_sensors:
read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect.extend([ProtocolError("Missing injected value")] * 20)
owproxy.return_value.dir.return_value = dir_return_value
owproxy.return_value.read.side_effect = read_side_effect
with patch("homeassistant.components.onewire.PLATFORMS", [platform]):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_sensors)
if len(expected_sensors) > 0:
device_info = mock_device_sensor["device_info"]
assert len(device_registry.devices) == 1
registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
assert registry_entry is not None
assert registry_entry.identifiers == {(DOMAIN, device_id)}
assert registry_entry.manufacturer == device_info["manufacturer"]
assert registry_entry.name == device_info["name"]
assert registry_entry.model == device_info["model"]
for expected_sensor in expected_sensors:
entity_id = expected_sensor["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
assert registry_entry.unique_id == expected_sensor["unique_id"]
assert registry_entry.unit_of_measurement == expected_sensor["unit"]
assert registry_entry.device_class == expected_sensor["class"]
assert registry_entry.disabled == expected_sensor.get("disabled", False)
state = hass.states.get(entity_id)
if registry_entry.disabled:
assert state is None
else:
assert state.state == expected_sensor["result"]
assert state.attributes["device_file"] == expected_sensor.get(
"device_file", registry_entry.unique_id
)
MOCK_SYSBUS_DEVICES = {
"00-111111111111": {"sensors": []},
"10-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "10-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "10",
"name": "10-111111111111",
},
"sensors": [
{
"entity_id": "sensor.my_ds18b20_temperature",
"unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave",
"injected_value": 25.123,
"result": "25.1",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"12-111111111111": {"sensors": []},
"1D-111111111111": {"sensors": []},
"22-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "22-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "22",
"name": "22-111111111111",
},
"sensors": [
{
"entity_id": "sensor.22_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave",
"injected_value": FileNotFoundError,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"26-111111111111": {"sensors": []},
"28-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "28-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "28",
"name": "28-111111111111",
},
"sensors": [
{
"entity_id": "sensor.28_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave",
"injected_value": InvalidCRCException,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"29-111111111111": {"sensors": []},
"3B-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "3B-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "3B",
"name": "3B-111111111111",
},
"sensors": [
{
"entity_id": "sensor.3b_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave",
"injected_value": 29.993,
"result": "30.0",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"42-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "42-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "42",
"name": "42-111111111111",
},
"sensors": [
{
"entity_id": "sensor.42_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave",
"injected_value": UnsupportResponseException,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"EF-111111111111": {
"sensors": [],
},
"EF-111111111112": {
"sensors": [],
},
}

View File

@@ -2,40 +2,25 @@
import copy
from unittest.mock import patch
from pyownet.protocol import Error as ProtocolError
import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.onewire.binary_sensor import DEVICE_BINARY_SENSORS
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component
from . import setup_onewire_patched_owserver_integration
from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices
from .const import MOCK_OWPROXY_DEVICES
from tests.common import mock_registry
MOCK_DEVICE_SENSORS = {
"12.111111111111": {
"inject_reads": [
b"DS2406", # read device type
],
BINARY_SENSOR_DOMAIN: [
{
"entity_id": "binary_sensor.12_111111111111_sensed_a",
"injected_value": b" 1",
"result": STATE_ON,
},
{
"entity_id": "binary_sensor.12_111111111111_sensed_b",
"injected_value": b" 0",
"result": STATE_OFF,
},
],
},
MOCK_BINARY_SENSORS = {
key: value
for (key, value) in MOCK_OWPROXY_DEVICES.items()
if BINARY_SENSOR_DOMAIN in value
}
@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys())
@pytest.mark.parametrize("device_id", MOCK_BINARY_SENSORS.keys())
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_owserver_binary_sensor(owproxy, hass, device_id):
"""Test for 1-Wire binary sensor.
@@ -45,26 +30,14 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id):
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id])
device_family = device_id[0:2]
dir_return_value = [f"/{device_id}/"]
read_side_effect = [device_family.encode()]
if "inject_reads" in mock_device_sensor:
read_side_effect += mock_device_sensor["inject_reads"]
expected_sensors = mock_device_sensor[BINARY_SENSOR_DOMAIN]
for expected_sensor in expected_sensors:
read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect.extend([ProtocolError("Missing injected value")] * 10)
owproxy.return_value.dir.return_value = dir_return_value
owproxy.return_value.read.side_effect = read_side_effect
mock_device = MOCK_BINARY_SENSORS[device_id]
expected_entities = mock_device[BINARY_SENSOR_DOMAIN]
# Force enable binary sensors
patch_device_binary_sensors = copy.deepcopy(DEVICE_BINARY_SENSORS)
for item in patch_device_binary_sensors[device_family]:
for item in patch_device_binary_sensors[device_id[0:2]]:
item["default_disabled"] = False
with patch(
@@ -76,14 +49,14 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_sensors)
assert len(entity_registry.entities) == len(expected_entities)
for expected_sensor in expected_sensors:
entity_id = expected_sensor["entity_id"]
for expected_entity in expected_entities:
entity_id = expected_entity["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
state = hass.states.get(entity_id)
assert state.state == expected_sensor["result"]
assert state.attributes["device_file"] == expected_sensor.get(
assert state.state == expected_entity["result"]
assert state.attributes["device_file"] == expected_entity.get(
"device_file", registry_entry.unique_id
)

View File

@@ -1,175 +0,0 @@
"""Tests for 1-Wire devices connected on SysBus."""
from unittest.mock import patch
from pi1wire import InvalidCRCException, UnsupportResponseException
import pytest
from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS
from homeassistant.setup import async_setup_component
from tests.common import mock_device_registry, mock_registry
MOCK_CONFIG = {
SENSOR_DOMAIN: {
"platform": DOMAIN,
"mount_dir": DEFAULT_SYSBUS_MOUNT_DIR,
"names": {
"10-111111111111": "My DS18B20",
},
}
}
MOCK_DEVICE_SENSORS = {
"00-111111111111": {"sensors": []},
"10-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "10-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "10",
"name": "10-111111111111",
},
"sensors": [
{
"entity_id": "sensor.my_ds18b20_temperature",
"unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave",
"injected_value": 25.123,
"result": "25.1",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"12-111111111111": {"sensors": []},
"1D-111111111111": {"sensors": []},
"22-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "22-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "22",
"name": "22-111111111111",
},
"sensors": [
{
"entity_id": "sensor.22_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave",
"injected_value": FileNotFoundError,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"26-111111111111": {"sensors": []},
"28-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "28-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "28",
"name": "28-111111111111",
},
"sensors": [
{
"entity_id": "sensor.28_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave",
"injected_value": InvalidCRCException,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"29-111111111111": {"sensors": []},
"3B-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "3B-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "3B",
"name": "3B-111111111111",
},
"sensors": [
{
"entity_id": "sensor.3b_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave",
"injected_value": 29.993,
"result": "30.0",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"42-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "42-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "42",
"name": "42-111111111111",
},
"sensors": [
{
"entity_id": "sensor.42_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave",
"injected_value": UnsupportResponseException,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"EF-111111111111": {
"sensors": [],
},
"EF-111111111112": {
"sensors": [],
},
}
@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys())
async def test_onewiredirect_setup_valid_device(hass, device_id):
"""Test that sysbus config entry works correctly."""
entity_registry = mock_registry(hass)
device_registry = mock_device_registry(hass)
mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
glob_result = [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"]
read_side_effect = []
expected_sensors = mock_device_sensor["sensors"]
for expected_sensor in expected_sensors:
read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20)
with patch(
"homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True
), patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch(
"pi1wire.OneWire.get_temperature",
side_effect=read_side_effect,
):
assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_CONFIG)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_sensors)
if len(expected_sensors) > 0:
device_info = mock_device_sensor["device_info"]
assert len(device_registry.devices) == 1
registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
assert registry_entry is not None
assert registry_entry.identifiers == {(DOMAIN, device_id)}
assert registry_entry.manufacturer == device_info["manufacturer"]
assert registry_entry.name == device_info["name"]
assert registry_entry.model == device_info["model"]
for expected_sensor in expected_sensors:
entity_id = expected_sensor["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
assert registry_entry.unique_id == expected_sensor["unique_id"]
assert registry_entry.unit_of_measurement == expected_sensor["unit"]
assert registry_entry.device_class == expected_sensor["class"]
state = hass.states.get(entity_id)
assert state.state == expected_sensor["result"]

View File

@@ -4,6 +4,7 @@ from unittest.mock import patch
from pyownet.protocol import ConnError, OwnetError
from homeassistant.components.onewire.const import CONF_TYPE_OWSERVER, DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import (
CONN_CLASS_LOCAL_POLL,
ENTRY_STATE_LOADED,
@@ -11,10 +12,17 @@ from homeassistant.config_entries import (
ENTRY_STATE_SETUP_RETRY,
)
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from . import setup_onewire_owserver_integration, setup_onewire_sysbus_integration
from . import (
setup_onewire_owserver_integration,
setup_onewire_patched_owserver_integration,
setup_onewire_sysbus_integration,
setup_owproxy_mock_devices,
)
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
async def test_owserver_connect_failure(hass):
@@ -87,3 +95,41 @@ async def test_unload_entry(hass):
assert config_entry_owserver.state == ENTRY_STATE_NOT_LOADED
assert config_entry_sysbus.state == ENTRY_STATE_NOT_LOADED
assert not hass.data.get(DOMAIN)
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_registry_cleanup(owproxy, hass):
"""Test for 1-Wire device.
As they would be on a clean setup: all binary-sensors and switches disabled.
"""
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
device_registry = mock_device_registry(hass)
# Initialise with two components
setup_owproxy_mock_devices(
owproxy, SENSOR_DOMAIN, ["10.111111111111", "28.111111111111"]
)
with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2
assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 2
# Second item has disappeared from bus, and was removed manually from the front-end
setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, ["10.111111111111"])
entity_registry.async_remove("sensor.28_111111111111_temperature")
await hass.async_block_till_done()
assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1
assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2
# Second item has disappeared from bus, and was removed manually from the front-end
with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]):
await hass.config_entries.async_reload("2")
await hass.async_block_till_done()
assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1
assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 1

View File

@@ -4,54 +4,29 @@ from unittest.mock import patch
from pyownet.protocol import Error as ProtocolError
import pytest
from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN
from homeassistant.components.onewire.const import (
DEFAULT_SYSBUS_MOUNT_DIR,
DOMAIN,
PLATFORMS,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.setup import async_setup_component
from . import setup_onewire_patched_owserver_integration
from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices
from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES
from tests.common import assert_setup_component, mock_registry
from tests.common import assert_setup_component, mock_device_registry, mock_registry
MOCK_COUPLERS = {
"1F.111111111111": {
"inject_reads": [
b"DS2409", # read device type
],
"branches": {
"aux": {},
"main": {
"1D.111111111111": {
"inject_reads": [
b"DS2423", # read device type
],
"device_info": {
"identifiers": {(DOMAIN, "1D.111111111111")},
"manufacturer": "Maxim Integrated",
"model": "DS2423",
"name": "1D.111111111111",
},
SENSOR_DOMAIN: [
{
"entity_id": "sensor.1d_111111111111_counter_a",
"device_file": "/1F.111111111111/main/1D.111111111111/counter.A",
"unique_id": "/1D.111111111111/counter.A",
"injected_value": b" 251123",
"result": "251123",
"unit": "count",
"class": None,
},
{
"entity_id": "sensor.1d_111111111111_counter_b",
"device_file": "/1F.111111111111/main/1D.111111111111/counter.B",
"unique_id": "/1D.111111111111/counter.B",
"injected_value": b" 248125",
"result": "248125",
"unit": "count",
"class": None,
},
],
},
},
key: value for (key, value) in MOCK_OWPROXY_DEVICES.items() if "branches" in value
}
MOCK_SYSBUS_CONFIG = {
SENSOR_DOMAIN: {
"platform": DOMAIN,
"mount_dir": DEFAULT_SYSBUS_MOUNT_DIR,
"names": {
"10-111111111111": "My DS18B20",
},
}
}
@@ -154,3 +129,103 @@ async def test_sensors_on_owserver_coupler(owproxy, hass, device_id):
else:
assert state.state == expected_sensor["result"]
assert state.attributes["device_file"] == expected_sensor["device_file"]
@pytest.mark.parametrize("device_id", MOCK_OWPROXY_DEVICES.keys())
@pytest.mark.parametrize("platform", PLATFORMS)
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform):
"""Test for 1-Wire device.
As they would be on a clean setup: all binary-sensors and switches disabled.
"""
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
device_registry = mock_device_registry(hass)
setup_owproxy_mock_devices(owproxy, platform, [device_id])
mock_device = MOCK_OWPROXY_DEVICES[device_id]
expected_entities = mock_device.get(platform, [])
with patch("homeassistant.components.onewire.PLATFORMS", [platform]):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_entities)
if len(expected_entities) > 0:
device_info = mock_device["device_info"]
assert len(device_registry.devices) == 1
registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
assert registry_entry is not None
assert registry_entry.identifiers == {(DOMAIN, device_id)}
assert registry_entry.manufacturer == device_info["manufacturer"]
assert registry_entry.name == device_info["name"]
assert registry_entry.model == device_info["model"]
for expected_entity in expected_entities:
entity_id = expected_entity["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
assert registry_entry.unique_id == expected_entity["unique_id"]
assert registry_entry.unit_of_measurement == expected_entity["unit"]
assert registry_entry.device_class == expected_entity["class"]
assert registry_entry.disabled == expected_entity.get("disabled", False)
state = hass.states.get(entity_id)
if registry_entry.disabled:
assert state is None
else:
assert state.state == expected_entity["result"]
assert state.attributes["device_file"] == expected_entity.get(
"device_file", registry_entry.unique_id
)
@pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys())
async def test_onewiredirect_setup_valid_device(hass, device_id):
"""Test that sysbus config entry works correctly."""
entity_registry = mock_registry(hass)
device_registry = mock_device_registry(hass)
mock_device_sensor = MOCK_SYSBUS_DEVICES[device_id]
glob_result = [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"]
read_side_effect = []
expected_sensors = mock_device_sensor["sensors"]
for expected_sensor in expected_sensors:
read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20)
with patch(
"homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True
), patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch(
"pi1wire.OneWire.get_temperature",
side_effect=read_side_effect,
):
assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_SYSBUS_CONFIG)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_sensors)
if len(expected_sensors) > 0:
device_info = mock_device_sensor["device_info"]
assert len(device_registry.devices) == 1
registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
assert registry_entry is not None
assert registry_entry.identifiers == {(DOMAIN, device_id)}
assert registry_entry.manufacturer == device_info["manufacturer"]
assert registry_entry.name == device_info["name"]
assert registry_entry.model == device_info["model"]
for expected_sensor in expected_sensors:
entity_id = expected_sensor["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
assert registry_entry.unique_id == expected_sensor["unique_id"]
assert registry_entry.unit_of_measurement == expected_sensor["unit"]
assert registry_entry.device_class == expected_sensor["class"]
state = hass.states.get(entity_id)
assert state.state == expected_sensor["result"]

View File

@@ -2,7 +2,6 @@
import copy
from unittest.mock import patch
from pyownet.protocol import Error as ProtocolError
import pytest
from homeassistant.components.onewire.switch import DEVICE_SWITCHES
@@ -10,58 +9,19 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TOGGLE, STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component
from . import setup_onewire_patched_owserver_integration
from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices
from .const import MOCK_OWPROXY_DEVICES
from tests.common import mock_registry
MOCK_DEVICE_SENSORS = {
"12.111111111111": {
"inject_reads": [
b"DS2406", # read device type
],
SWITCH_DOMAIN: [
{
"entity_id": "switch.12_111111111111_pio_a",
"unique_id": "/12.111111111111/PIO.A",
"injected_value": b" 1",
"result": STATE_ON,
"unit": None,
"class": None,
"disabled": True,
},
{
"entity_id": "switch.12_111111111111_pio_b",
"unique_id": "/12.111111111111/PIO.B",
"injected_value": b" 0",
"result": STATE_OFF,
"unit": None,
"class": None,
"disabled": True,
},
{
"entity_id": "switch.12_111111111111_latch_a",
"unique_id": "/12.111111111111/latch.A",
"injected_value": b" 1",
"result": STATE_ON,
"unit": None,
"class": None,
"disabled": True,
},
{
"entity_id": "switch.12_111111111111_latch_b",
"unique_id": "/12.111111111111/latch.B",
"injected_value": b" 0",
"result": STATE_OFF,
"unit": None,
"class": None,
"disabled": True,
},
],
}
MOCK_SWITCHES = {
key: value
for (key, value) in MOCK_OWPROXY_DEVICES.items()
if SWITCH_DOMAIN in value
}
@pytest.mark.parametrize("device_id", ["12.111111111111"])
@pytest.mark.parametrize("device_id", MOCK_SWITCHES.keys())
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_owserver_switch(owproxy, hass, device_id):
"""Test for 1-Wire switch.
@@ -71,26 +31,14 @@ async def test_owserver_switch(owproxy, hass, device_id):
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id])
device_family = device_id[0:2]
dir_return_value = [f"/{device_id}/"]
read_side_effect = [device_family.encode()]
if "inject_reads" in mock_device_sensor:
read_side_effect += mock_device_sensor["inject_reads"]
expected_sensors = mock_device_sensor[SWITCH_DOMAIN]
for expected_sensor in expected_sensors:
read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect.extend([ProtocolError("Missing injected value")] * 10)
owproxy.return_value.dir.return_value = dir_return_value
owproxy.return_value.read.side_effect = read_side_effect
mock_device = MOCK_SWITCHES[device_id]
expected_entities = mock_device[SWITCH_DOMAIN]
# Force enable switches
patch_device_switches = copy.deepcopy(DEVICE_SWITCHES)
for item in patch_device_switches[device_family]:
for item in patch_device_switches[device_id[0:2]]:
item["default_disabled"] = False
with patch(
@@ -101,21 +49,21 @@ async def test_owserver_switch(owproxy, hass, device_id):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_sensors)
assert len(entity_registry.entities) == len(expected_entities)
for expected_sensor in expected_sensors:
entity_id = expected_sensor["entity_id"]
for expected_entity in expected_entities:
entity_id = expected_entity["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
state = hass.states.get(entity_id)
assert state.state == expected_sensor["result"]
assert state.state == expected_entity["result"]
if state.state == STATE_ON:
owproxy.return_value.read.side_effect = [b" 0"]
expected_sensor["result"] = STATE_OFF
expected_entity["result"] = STATE_OFF
elif state.state == STATE_OFF:
owproxy.return_value.read.side_effect = [b" 1"]
expected_sensor["result"] = STATE_ON
expected_entity["result"] = STATE_ON
await hass.services.async_call(
SWITCH_DOMAIN,
@@ -126,7 +74,7 @@ async def test_owserver_switch(owproxy, hass, device_id):
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == expected_sensor["result"]
assert state.attributes["device_file"] == expected_sensor.get(
assert state.state == expected_entity["result"]
assert state.attributes["device_file"] == expected_entity.get(
"device_file", registry_entry.unique_id
)

Some files were not shown because too many files have changed in this diff Show More