Compare commits

...

92 Commits

Author SHA1 Message Date
Paulus Schoutsen b5548c57fb Merge pull request #49139 from home-assistant/rc 2021-04-12 17:45:38 -07:00
Paulus Schoutsen e5281051a3 Bumped version to 2021.4.4 2021-04-13 00:10:42 +00:00
jjlawren 346ae78a8e Check all endpoints for zwave_js.climate hvac_action (#49115) 2021-04-13 00:09:51 +00:00
Unai b5650bdd52 Upgrade maxcube-api to 0.4.2 (#49106)
Upgrade to maxcube-api 0.4.2 to fix pending issues in HA 2021.4.x:
 - Interpret correctly S command error responses (https://github.com/home-assistant/core/issues/49075)
 - Support application timezone configuration (https://github.com/home-assistant/core/issues/49076)
2021-04-13 00:09:50 +00:00
J. Nick Koston bf28268732 Downgrade logger message about homekit id missing (#49079)
This can happen if the TXT record is received after the PTR record and
should not generate a warning since it will get processed later
2021-04-13 00:09:49 +00:00
Kevin Worrel 4eb794ae84 Catch unknown equipment values (#49073)
* Catch unknown equipment values

* Catch unknown equipment values

* Remove warning spam.
2021-04-13 00:09:48 +00:00
Jan Bouwhuis 21b5551506 mqtt fan percentage to speed_range and received speed_state fix (#49060)
* percentage to speed_range and get speed state fix

* Update homeassistant/components/mqtt/fan.py

* Update homeassistant/components/mqtt/fan.py

* Update homeassistant/components/mqtt/fan.py

* Update homeassistant/components/mqtt/fan.py

Co-authored-by: J. Nick Koston <nick@koston.org>
2021-04-13 00:07:27 +00:00
Erik Montnemery e0131f726f Quote media_source paths (#49054)
* Quote path in async_sign_path

* Address review comments, add tests

* Update tests/testing_config/media/Epic Sax Guy 10 Hours.mp4

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2021-04-13 00:07:27 +00:00
Erik Montnemery e685b1a1e3 Fix cast options flow overwriting data (#49051) 2021-04-13 00:07:26 +00:00
J. Nick Koston 0d00e49dfc Bump aiohomekit to 0.2.61 (#49044) 2021-04-13 00:07:25 +00:00
Chris Talkington 36e08e770b Resolve potential roku setup memory leaks (#49025)
* resolve potential roku setup memory leaks

* Update __init__.py
2021-04-13 00:07:24 +00:00
Franck Nijhof e3b3d136d8 Fix use search instead of match to filter logs (#49017) 2021-04-13 00:07:23 +00:00
Joakim Plate 0a5a5ff053 Bump ha-philipsjs to 2.7.0 (#49008)
This has some improvements to not consider the TV
off due to some exceptions that is related to API
being buggy rather than off.
2021-04-13 00:07:22 +00:00
Shay Levy 0bb7592fab Fix Shelly brightness offset (#49007) 2021-04-13 00:07:21 +00:00
Aidan Timson d081ac8d4a Set Lyric hold time to use local time instead of utc (#48994) 2021-04-13 00:07:20 +00:00
J. Nick Koston b96e0e69f2 Bump nexia to 0.9.6 (#48982)
- Now returns None when a humidity sensor cannot be read instead of throwing an exception
2021-04-13 00:07:20 +00:00
Erik Montnemery 82cca8fb1c Move cast config flow tests to test_config_flow (#48362) 2021-04-13 00:07:19 +00:00
Matt Zimmerman a9602e7a08 Update python-smarttub to 0.0.23 (#48978) 2021-04-10 03:38:13 +00:00
Matt Zimmerman c08ae64085 Update python-smarttub to 0.0.23 (#48978) 2021-04-10 03:37:10 +00:00
Paulus Schoutsen 01e558430a Merge pull request #48977 from home-assistant/rc 2021-04-09 20:35:34 -07:00
Paulus Schoutsen 31b061e8f1 Bumped version to 2021.4.3 2021-04-10 00:30:34 +00:00
Shay Levy 4ca40367d1 Fix Shelly button device triggers (#48974) 2021-04-10 00:30:24 +00:00
J. Nick Koston 12da88cae9 Prevent ping id allocation conflict with device_tracker (#48969)
* Prevent ping id allocation conflict with device_tracker

- Solves id conflict resulting unexpected home state

* Update homeassistant/components/ping/device_tracker.py

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2021-04-10 00:30:23 +00:00
Paulus Schoutsen 0520ce5ed3 Fix config forwarding (#48967) 2021-04-10 00:30:22 +00:00
Guido Schmitz 995e22d3bb Bump devolo Home Control to support old websocket-client versions again (#48960) 2021-04-10 00:30:21 +00:00
Jan Bouwhuis 6296d78e58 Implement percentage_step and preset_mode is not not speed fix for MQTT fan (#48951) 2021-04-10 00:30:20 +00:00
Joakim Sørensen 2c7fd30029 Add TTS engines in config.components (#48939)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-10 00:30:20 +00:00
Paulus Schoutsen 7b7cfd71de Merge pull request #48954 from home-assistant/rc 2021-04-09 12:03:40 -07:00
Bram Kragten d5d9a5ff11 Update frontend to 20210407.3 (#48957) 2021-04-09 18:53:45 +00:00
Paulus Schoutsen 3f744bcbef Bumped version to 2021.4.2 2021-04-09 17:39:03 +00:00
jjlawren 92746aa60c Fix Plex live TV handling (#48953) 2021-04-09 17:38:54 +00:00
Ph-Wagner 4da77b9768 Extend Google Cast media source URL expiry to 24h (#48937)
* Extend media source URL expiry to 12h

closes #46280
After checking out https://github.com/home-assistant/core/pull/48912 I just think why not.

* Update homeassistant/components/cast/media_player.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2021-04-09 17:38:53 +00:00
David F. Mulcahey b800bb0202 Bump ZHA quirks library (#48931) 2021-04-09 17:38:52 +00:00
Tobias Sauerwein b41e611cb5 Bump pykodi to 0.2.5 (#48930) 2021-04-09 17:38:52 +00:00
Philip Allgaier ee78c9b08a Fix "notify.events" trim() issue + add initial tests (#48928)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-09 17:38:51 +00:00
Joakim Sørensen 29bb6d76f1 Change discovery timeout from 10 to 60 (#48924) 2021-04-09 17:38:50 +00:00
Joakim Sørensen 38a1c65ab7 Handle exceptions when looking for new version (#48922) 2021-04-09 17:38:49 +00:00
Tobias Sauerwein 1ca087b4d0 Bump pykodi to 0.2.4 (#48913) 2021-04-09 17:38:48 +00:00
Erik Montnemery 1f21b19eae Extend media source URL expiry to 24h (#48912) 2021-04-09 17:38:47 +00:00
Milan Meulemans 6746fbadef Catch expected errors and log them in rituals perfume genie (#48870)
* Add update error logging

* Move try available to else

* Remove TimeoutError
2021-04-09 17:38:46 +00:00
Hans Kröner 41fe8b9494 Account for openweathermap 'dew_point' not always being present (#48826) 2021-04-09 17:38:45 +00:00
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
124 changed files with 2983 additions and 1696 deletions
@@ -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
@@ -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},
)
@@ -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
+2 -1
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"
+1 -1
View File
@@ -133,7 +133,7 @@ class CastOptionsFlowHandler(config_entries.OptionsFlow):
)
if not bad_cec and not bad_hosts and not bad_uuid:
updated_config = {}
updated_config = dict(current_config)
updated_config[CONF_IGNORE_CEC] = ignore_cec
updated_config[CONF_KNOWN_HOSTS] = known_hosts
updated_config[CONF_UUID] = wanted_uuid
+1 -1
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"]
@@ -1,11 +1,13 @@
"""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
import json
import logging
from urllib.parse import quote
import pychromecast
from pychromecast.controllers.homeassistant import HomeAssistantController
@@ -185,7 +187,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())
)
@@ -469,8 +473,8 @@ class CastDevice(MediaPlayerEntity):
media_id = async_sign_path(
self.hass,
refresh_token.id,
media_id,
timedelta(minutes=5),
quote(media_id),
timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME),
)
# prepend external URL
@@ -2,7 +2,7 @@
"domain": "devolo_home_control",
"name": "devolo Home Control",
"documentation": "https://www.home-assistant.io/integrations/devolo_home_control",
"requirements": ["devolo-home-control-api==0.17.1"],
"requirements": ["devolo-home-control-api==0.17.3"],
"after_dependencies": ["zeroconf"],
"config_flow": true,
"codeowners": ["@2Fake", "@Shutgun"],
+1 -1
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"
@@ -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. "
@@ -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()
+26 -4
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):
@@ -62,7 +62,7 @@ MANIFEST_JSON = {
"screenshots": [
{
"src": "/static/images/screenshots/screenshot-1.png",
"sizes": "413×792",
"sizes": "413x792",
"type": "image/png",
}
],
@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20210402.0"
"home-assistant-frontend==20210407.3"
],
"dependencies": [
"api",
@@ -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)
@@ -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={})
+1 -1
View File
@@ -148,7 +148,7 @@ class HassIO:
This method return a coroutine.
"""
return self.send_command("/discovery", method="get")
return self.send_command("/discovery", method="get", timeout=60)
@api_data
def get_discovery_message(self, uuid):
+1 -1
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
@@ -209,8 +209,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
}
if "id" not in properties:
_LOGGER.warning(
"HomeKit device %s: id not exposed, in violation of spec", properties
# This can happen if the TXT record is received after the PTR record
# we will wait for the next update in this case
_LOGGER.debug(
"HomeKit device %s: id not exposed; TXT record may have not yet been received",
properties,
)
return self.async_abort(reason="invalid_properties")
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": [
"aiohomekit==0.2.60"
"aiohomekit==0.2.61"
],
"zeroconf": [
"_hap._tcp.local."
+8 -2
View File
@@ -1,6 +1,7 @@
"""Authentication for HTTP component."""
import logging
import secrets
from urllib.parse import unquote
from aiohttp import hdrs
from aiohttp.web import middleware
@@ -30,11 +31,16 @@ def async_sign_path(hass, refresh_token_id, path, expiration):
now = dt_util.utcnow()
encoded = jwt.encode(
{"iss": refresh_token_id, "path": path, "iat": now, "exp": now + expiration},
{
"iss": refresh_token_id,
"path": unquote(path),
"iat": now,
"exp": now + expiration,
},
secret,
algorithm="HS256",
)
return f"{path}?{SIGN_QUERY_PARAM}=" f"{encoded.decode()}"
return f"{path}?{SIGN_QUERY_PARAM}={encoded.decode()}"
@callback
+1 -1
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
@@ -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]:
+1 -1
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]:
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Kodi",
"documentation": "https://www.home-assistant.io/integrations/kodi",
"requirements": [
"pykodi==0.2.3"
"pykodi==0.2.5"
],
"codeowners": [
"@OnFreund",
@@ -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"
+1 -1
View File
@@ -114,7 +114,7 @@ def _add_log_filter(logger, patterns):
"""Add a Filter to the logger based on a regexp of the filter_str."""
def filter_func(logrecord):
return not any(p.match(logrecord.getMessage()) for p in patterns)
return not any(p.search(logrecord.getMessage()) for p in patterns)
logger.addFilter(filter_func)
+2 -2
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
import logging
from time import gmtime, strftime, time
from time import localtime, strftime, time
from aiolyric.objects.device import LyricDevice
from aiolyric.objects.location import LyricLocation
@@ -82,7 +82,7 @@ SCHEMA_HOLD_TIME = {
vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All(
cv.time_period,
cv.positive_timedelta,
lambda td: strftime("%H:%M:%S", gmtime(time() + td.total_seconds())),
lambda td: strftime("%H:%M:%S", localtime(time() + td.total_seconds())),
)
}
+2 -1
View File
@@ -10,6 +10,7 @@ import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.util.dt import now
_LOGGER = logging.getLogger(__name__)
@@ -59,7 +60,7 @@ def setup(hass, config):
scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds()
try:
cube = MaxCube(host, port)
cube = MaxCube(host, port, now=now)
hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval)
except timeout as ex:
_LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex))
@@ -2,6 +2,6 @@
"domain": "maxcube",
"name": "eQ-3 MAX!",
"documentation": "https://www.home-assistant.io/integrations/maxcube",
"requirements": ["maxcube-api==0.4.1"],
"requirements": ["maxcube-api==0.4.2"],
"codeowners": []
}
@@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import timedelta
from urllib.parse import quote
import voluptuous as vol
@@ -19,6 +20,8 @@ from . import local_source, models
from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX
from .error import Unresolvable
DEFAULT_EXPIRY_TIME = 3600 * 24
def is_media_source_id(media_content_id: str):
"""Test if identifier is a media source."""
@@ -105,7 +108,7 @@ async def websocket_browse_media(hass, connection, msg):
{
vol.Required("type"): "media_source/resolve_media",
vol.Required(ATTR_MEDIA_CONTENT_ID): str,
vol.Optional("expires", default=30): int,
vol.Optional("expires", default=DEFAULT_EXPIRY_TIME): int,
}
)
@websocket_api.async_response
@@ -121,7 +124,7 @@ async def websocket_resolve_media(hass, connection, msg):
url = async_sign_path(
hass,
connection.refresh_token_id,
url,
quote(url),
timedelta(seconds=msg["expires"]),
)
+30 -6
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)."""
+15 -1
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}
)
+3
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 = {
+6 -1
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"
}
}
}
@@ -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):
+28 -70
View File
@@ -1,6 +1,7 @@
"""Support for MQTT fans."""
import functools
import logging
import math
import voluptuous as vol
@@ -32,6 +33,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util.percentage import (
int_states_in_range,
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
percentage_to_ranged_value,
@@ -224,6 +226,9 @@ class MqttFan(MqttEntity, FanEntity):
self._optimistic_preset_mode = None
self._optimistic_speed = None
self._legacy_speeds_list = []
self._legacy_speeds_list_no_off = []
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@staticmethod
@@ -284,28 +289,18 @@ class MqttFan(MqttEntity, FanEntity):
self._legacy_speeds_list_no_off = speed_list_without_preset_modes(
self._legacy_speeds_list
)
else:
self._legacy_speeds_list = []
self._feature_percentage = CONF_PERCENTAGE_COMMAND_TOPIC in config
self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config
if self._feature_preset_mode:
self._speeds_list = speed_list_without_preset_modes(
self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST]
)
self._preset_modes = (
self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST]
)
self._preset_modes = config[CONF_PRESET_MODES_LIST]
else:
self._speeds_list = speed_list_without_preset_modes(
self._legacy_speeds_list
)
self._preset_modes = []
if not self._speeds_list or self._feature_percentage:
self._speed_count = 100
if self._feature_percentage:
self._speed_count = min(int_states_in_range(self._speed_range), 100)
else:
self._speed_count = len(self._speeds_list)
self._speed_count = len(self._legacy_speeds_list_no_off) or 100
optimistic = config[CONF_OPTIMISTIC]
self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None
@@ -327,11 +322,7 @@ class MqttFan(MqttEntity, FanEntity):
self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None
and SUPPORT_OSCILLATE
)
if self._feature_preset_mode and self._speeds_list:
self._supported_features |= SUPPORT_SET_SPEED
if self._feature_percentage:
self._supported_features |= SUPPORT_SET_SPEED
if self._feature_legacy_speeds:
if self._feature_percentage or self._feature_legacy_speeds:
self._supported_features |= SUPPORT_SET_SPEED
if self._feature_preset_mode:
self._supported_features |= SUPPORT_PRESET_MODE
@@ -414,10 +405,6 @@ class MqttFan(MqttEntity, FanEntity):
return
self._preset_mode = preset_mode
if not self._implemented_percentage and (preset_mode in self.speed_list):
self._percentage = ordered_list_item_to_percentage(
self.speed_list, preset_mode
)
self.async_write_ha_state()
if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None:
@@ -455,13 +442,12 @@ class MqttFan(MqttEntity, FanEntity):
)
return
if not self._implemented_percentage:
if speed in self._speeds_list:
self._percentage = ordered_list_item_to_percentage(
self._speeds_list, speed
)
elif speed == SPEED_OFF:
self._percentage = 0
if speed in self._legacy_speeds_list_no_off:
self._percentage = ordered_list_item_to_percentage(
self._legacy_speeds_list_no_off, speed
)
elif speed == SPEED_OFF:
self._percentage = 0
self.async_write_ha_state()
@@ -506,19 +492,9 @@ class MqttFan(MqttEntity, FanEntity):
"""Return true if device is on."""
return self._state
@property
def _implemented_percentage(self):
"""Return true if percentage has been implemented."""
return self._feature_percentage
@property
def _implemented_preset_mode(self):
"""Return true if preset_mode has been implemented."""
return self._feature_preset_mode
# The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
@property
def _implemented_speed(self):
def _implemented_speed(self) -> bool:
"""Return true if speed has been implemented."""
return self._feature_legacy_speeds
@@ -541,7 +517,7 @@ class MqttFan(MqttEntity, FanEntity):
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
return self._speeds_list
return self._legacy_speeds_list_no_off
@property
def supported_features(self) -> int:
@@ -555,7 +531,7 @@ class MqttFan(MqttEntity, FanEntity):
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports or 100 if percentage is supported."""
"""Return the number of speeds the fan supports."""
return self._speed_count
@property
@@ -616,24 +592,12 @@ class MqttFan(MqttEntity, FanEntity):
This method is a coroutine.
"""
percentage_payload = int(
percentage_payload = math.ceil(
percentage_to_ranged_value(self._speed_range, percentage)
)
mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload)
if self._implemented_preset_mode:
if percentage:
await self.async_set_preset_mode(
preset_mode=percentage_to_ordered_list_item(
self.speed_list, percentage
)
)
# Legacy are deprecated in the schema, support will be removed after a quarter (2021.7)
elif self._feature_legacy_speeds and (
SPEED_OFF in self._legacy_speeds_list
):
await self.async_set_preset_mode(SPEED_OFF)
# Legacy are deprecated in the schema, support will be removed after a quarter (2021.7)
elif self._feature_legacy_speeds:
if self._feature_legacy_speeds:
if percentage:
await self.async_set_speed(
percentage_to_ordered_list_item(
@@ -644,7 +608,7 @@ class MqttFan(MqttEntity, FanEntity):
elif SPEED_OFF in self._legacy_speeds_list:
await self.async_set_speed(SPEED_OFF)
if self._implemented_percentage:
if self._feature_percentage:
mqtt.async_publish(
self.hass,
self._topic[CONF_PERCENTAGE_COMMAND_TOPIC],
@@ -665,13 +629,7 @@ class MqttFan(MqttEntity, FanEntity):
if preset_mode not in self.preset_modes:
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
return
# Legacy are deprecated in the schema, support will be removed after a quarter (2021.7)
if preset_mode in self._legacy_speeds_list:
await self.async_set_speed(speed=preset_mode)
if not self._implemented_percentage and preset_mode in self.speed_list:
self._percentage = ordered_list_item_to_percentage(
self.speed_list, preset_mode
)
mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode)
mqtt.async_publish(
@@ -693,18 +651,18 @@ class MqttFan(MqttEntity, FanEntity):
This method is a coroutine.
"""
speed_payload = None
if self._feature_legacy_speeds:
if speed in self._legacy_speeds_list:
if speed == SPEED_LOW:
speed_payload = self._payload["SPEED_LOW"]
elif speed == SPEED_MEDIUM:
speed_payload = self._payload["SPEED_MEDIUM"]
elif speed == SPEED_HIGH:
speed_payload = self._payload["SPEED_HIGH"]
elif speed == SPEED_OFF:
speed_payload = self._payload["SPEED_OFF"]
else:
_LOGGER.warning("'%s'is not a valid speed", speed)
return
speed_payload = self._payload["SPEED_OFF"]
else:
_LOGGER.warning("'%s' is not a valid speed", speed)
return
if speed_payload:
mqtt.async_publish(
@@ -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
@@ -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:
+2 -2
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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"domain": "nexia",
"name": "Nexia",
"requirements": ["nexia==0.9.5"],
"requirements": ["nexia==0.9.6"],
"codeowners": ["@bdraco"],
"documentation": "https://www.home-assistant.io/integrations/nexia",
"config_flow": true,
+2
View File
@@ -13,4 +13,6 @@ def is_invalid_auth_code(http_status_code):
def percent_conv(val):
"""Convert an actual percentage (0.0-1.0) to 0-100 scale."""
if val is None:
return None
return round(val * 100.0, 1)
@@ -116,12 +116,9 @@ class NotifyEventsNotificationService(BaseNotificationService):
def send_message(self, message, **kwargs):
"""Send a message."""
token = self.token
data = kwargs.get(ATTR_DATA) or {}
token = data.get(ATTR_TOKEN, self.token)
msg = self.prepare_message(message, data)
if data.get(ATTR_TOKEN, "").trim():
token = data[ATTR_TOKEN]
msg.send(token)
@@ -122,7 +122,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
ATTR_API_FEELS_LIKE_TEMPERATURE: current_weather.temperature("celsius").get(
"feels_like"
),
ATTR_API_DEW_POINT: (round(current_weather.dewpoint / 100, 1)),
ATTR_API_DEW_POINT: self._fmt_dewpoint(current_weather.dewpoint),
ATTR_API_PRESSURE: current_weather.pressure.get("press"),
ATTR_API_HUMIDITY: current_weather.humidity,
ATTR_API_WIND_BEARING: current_weather.wind().get("deg"),
@@ -178,6 +178,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
return forecast
@staticmethod
def _fmt_dewpoint(dewpoint):
if dewpoint is not None:
return round(dewpoint / 100, 1)
return None
@staticmethod
def _get_rain(rain):
"""Get rain data from weather data."""
@@ -134,8 +134,12 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
async def _notify_task(self):
while self.api.on and self.api.notify_change_supported:
if await self.api.notifyChange(130):
res = await self.api.notifyChange(130)
if res:
self.async_set_updated_data(None)
elif res is None:
LOGGER.debug("Aborting notify due to unexpected return")
break
@callback
def _async_notify_stop(self):
@@ -3,7 +3,7 @@
"name": "Philips TV",
"documentation": "https://www.home-assistant.io/integrations/philips_js",
"requirements": [
"ha-philipsjs==2.3.2"
"ha-philipsjs==2.7.0"
],
"codeowners": [
"@elupus"
+12 -10
View File
@@ -24,20 +24,22 @@ async def async_setup(hass, config):
@callback
def async_get_next_ping_id(hass):
def async_get_next_ping_id(hass, count=1):
"""Find the next id to use in the outbound ping.
When using multiping, we increment the id
by the number of ids that multiping
will use.
Must be called in async
"""
current_id = hass.data[DOMAIN][PING_ID]
if current_id == MAX_PING_ID:
next_id = DEFAULT_START_ID
else:
next_id = current_id + 1
hass.data[DOMAIN][PING_ID] = next_id
return next_id
allocated_id = hass.data[DOMAIN][PING_ID] + 1
if allocated_id > MAX_PING_ID:
allocated_id -= MAX_PING_ID - DEFAULT_START_ID
hass.data[DOMAIN][PING_ID] += count
if hass.data[DOMAIN][PING_ID] > MAX_PING_ID:
hass.data[DOMAIN][PING_ID] -= MAX_PING_ID - DEFAULT_START_ID
return allocated_id
def _can_use_icmp_lib_with_privilege() -> None | bool:
@@ -125,7 +125,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
count=PING_ATTEMPTS_COUNT,
timeout=ICMP_TIMEOUT,
privileged=privileged,
id=async_get_next_ping_id(hass),
id=async_get_next_ping_id(hass, len(ip_to_dev_id)),
)
)
_LOGGER.debug("Multiping responses: %s", responses)
+1 -1
View File
@@ -7,7 +7,7 @@ from homeassistant.components.media_player.const import (
)
from homeassistant.util import dt as dt_util
LIVE_TV_SECTION = "-4"
LIVE_TV_SECTION = -4
class PlexSession:
+1 -1
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)
@@ -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}")
+10 -6
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
)
@@ -1,10 +1,15 @@
"""Support for Rituals Perfume Genie switches."""
from datetime import timedelta
import logging
import aiohttp
from homeassistant.components.switch import SwitchEntity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
ON_STATE = "1"
@@ -33,6 +38,7 @@ class DiffuserSwitch(SwitchEntity):
def __init__(self, diffuser):
"""Initialize the switch."""
self._diffuser = diffuser
self._available = True
@property
def device_info(self):
@@ -53,7 +59,7 @@ class DiffuserSwitch(SwitchEntity):
@property
def available(self):
"""Return if the device is available."""
return self._diffuser.data["hub"]["status"] == AVAILABLE_STATE
return self._available
@property
def name(self):
@@ -89,4 +95,10 @@ class DiffuserSwitch(SwitchEntity):
async def async_update(self):
"""Update the data of the device."""
await self._diffuser.update_data()
try:
await self._diffuser.update_data()
except aiohttp.ClientError:
self._available = False
_LOGGER.error("Unable to retrieve data from rituals.sense-company.com")
else:
self._available = self._diffuser.data["hub"]["status"] == AVAILABLE_STATE
+5 -3
View File
@@ -47,10 +47,12 @@ async def async_setup(hass: HomeAssistantType, config: dict) -> bool:
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up Roku from a config entry."""
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
await coordinator.async_config_entry_first_refresh()
coordinator = hass.data[DOMAIN].get(entry.entry_id)
if not coordinator:
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
hass.data[DOMAIN][entry.entry_id] = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
await coordinator.async_config_entry_first_refresh()
for platform in PLATFORMS:
hass.async_create_task(
@@ -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):
@@ -195,9 +195,15 @@ class ScreenlogicEntity(CoordinatorEntity):
"""Return device information for the controller."""
controller_type = self.config_data["controller_type"]
hardware_type = self.config_data["hardware_type"]
try:
equipment_model = EQUIPMENT.CONTROLLER_HARDWARE[controller_type][
hardware_type
]
except KeyError:
equipment_model = f"Unknown Model C:{controller_type} H:{hardware_type}"
return {
"connections": {(dr.CONNECTION_NETWORK_MAC, self.mac)},
"name": self.gateway_name,
"manufacturer": "Pentair",
"model": EQUIPMENT.CONTROLLER_HARDWARE[controller_type][hardware_type],
"model": equipment_model,
}
@@ -27,6 +27,7 @@ from .const import (
DOMAIN,
EVENT_SHELLY_CLICK,
INPUTS_EVENTS_SUBTYPES,
SHBTN_1_INPUTS_EVENTS_TYPES,
SUPPORTED_INPUTS_EVENTS_TYPES,
)
from .utils import get_device_wrapper, get_input_triggers
@@ -45,7 +46,7 @@ async def async_validate_trigger_config(hass, config):
# if device is available verify parameters against device capabilities
wrapper = get_device_wrapper(hass, config[CONF_DEVICE_ID])
if not wrapper:
if not wrapper or not wrapper.device.initialized:
return config
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
@@ -68,6 +69,19 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
if not wrapper:
raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}")
if wrapper.model in ("SHBTN-1", "SHBTN-2"):
for trigger in SHBTN_1_INPUTS_EVENTS_TYPES:
triggers.append(
{
CONF_PLATFORM: "device",
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_TYPE: trigger,
CONF_SUBTYPE: "button",
}
)
return triggers
for block in wrapper.device.blocks:
input_triggers = get_input_triggers(wrapper.device, block)
+9 -8
View File
@@ -118,15 +118,16 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
"""Brightness of light."""
if self.mode == "color":
if self.control_result:
brightness = self.control_result["gain"]
brightness_pct = self.control_result["gain"]
else:
brightness = self.block.gain
brightness_pct = self.block.gain
else:
if self.control_result:
brightness = self.control_result["brightness"]
brightness_pct = self.control_result["brightness"]
else:
brightness = self.block.brightness
return int(brightness / 100 * 255)
brightness_pct = self.block.brightness
return round(255 * brightness_pct / 100)
@property
def white_value(self) -> int:
@@ -188,11 +189,11 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
set_mode = None
params = {"turn": "on"}
if ATTR_BRIGHTNESS in kwargs:
tmp_brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100)
brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255)
if hasattr(self.block, "gain"):
params["gain"] = tmp_brightness
params["gain"] = brightness_pct
if hasattr(self.block, "brightness"):
params["brightness"] = tmp_brightness
params["brightness"] = brightness_pct
if ATTR_COLOR_TEMP in kwargs:
color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])
color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp))
@@ -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"]
}
@@ -6,7 +6,7 @@
"dependencies": [],
"codeowners": ["@mdz"],
"requirements": [
"python-smarttub==0.0.19"
"python-smarttub==0.0.23"
],
"quality_scale": "platinum"
}
@@ -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"]
}
+21 -3
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()
+8 -7
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."""
+2 -4
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]
+86 -23
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(config)
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],
+91 -12
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
@@ -20,3 +20,7 @@ PLATFORMS = [
"vacuum",
"weather",
]
CONF_AVAILABILITY = "availability"
CONF_ATTRIBUTES = "attributes"
CONF_PICTURE = "picture"
+8 -5
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)
@@ -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()
+5
View File
@@ -31,6 +31,7 @@ from homeassistant.const import (
CONF_PLATFORM,
HTTP_BAD_REQUEST,
HTTP_NOT_FOUND,
PLATFORM_FORMAT,
)
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
@@ -316,6 +317,10 @@ class SpeechManager:
provider.name = engine
self.providers[engine] = provider
self.hass.config.components.add(
PLATFORM_FORMAT.format(domain=engine, platform=DOMAIN)
)
async def async_get_url_path(
self, engine, message, cache=None, language=None, options=None
):
@@ -5,5 +5,6 @@
"requirements": ["mutagen==1.45.1"],
"dependencies": ["http"],
"after_dependencies": ["media_player"],
"quality_scale": "internal",
"codeowners": ["@pvizeli"]
}
@@ -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:
+8 -4
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)
+12 -1
View File
@@ -1,7 +1,9 @@
"""Sensor that can display the current Home Assistant versions."""
from datetime import timedelta
import logging
from pyhaversion import HaVersion, HaVersionChannel, HaVersionSource
from pyhaversion.exceptions import HaVersionFetchException, HaVersionParseException
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
@@ -59,6 +61,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
}
)
_LOGGER: logging.Logger = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Version sensor platform."""
@@ -114,7 +118,14 @@ class VersionData:
@Throttle(TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Get the latest version information."""
await self.api.get_version()
try:
await self.api.get_version()
except HaVersionFetchException as exception:
_LOGGER.warning(exception)
except HaVersionParseException as exception:
_LOGGER.warning(
"Could not parse data received for %s - %s", self.api.source, exception
)
class VersionSensor(SensorEntity):
@@ -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)
+1 -1
View File
@@ -7,7 +7,7 @@
"bellows==0.23.1",
"pyserial==3.5",
"pyserial-asyncio==0.5",
"zha-quirks==0.0.55",
"zha-quirks==0.0.56",
"zigpy-cc==0.5.2",
"zigpy-deconz==0.12.0",
"zigpy==0.33.0",
@@ -150,6 +150,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
THERMOSTAT_OPERATING_STATE_PROPERTY,
command_class=CommandClass.THERMOSTAT_OPERATING_STATE,
add_to_watched_value_ids=True,
check_all_endpoints=True,
)
self._current_temp = self.get_zwave_value(
THERMOSTAT_CURRENT_TEMP_PROPERTY,
@@ -169,11 +170,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
@@ -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"]
}
+11 -5
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(
+1 -1
View File
@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 2021
MINOR_VERSION = 4
PATCH_VERSION = "0b2"
PATCH_VERSION = "4"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 8, 0)
+61 -9
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
+2 -2
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==20210402.0
home-assistant-frontend==20210407.3
httpx==0.17.1
jinja2>=2.11.3
netdisco==2.8.2
+1
View File
@@ -36,6 +36,7 @@ BASE_PLATFORMS = {
"scene",
"sensor",
"switch",
"tts",
"vacuum",
"water_heater",
}
+14 -14
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
@@ -172,7 +172,7 @@ aioguardian==1.0.4
aioharmony==0.2.7
# homeassistant.components.homekit_controller
aiohomekit==0.2.60
aiohomekit==0.2.61
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -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
@@ -479,7 +479,7 @@ deluge-client==1.7.1
denonavr==0.9.10
# homeassistant.components.devolo_home_control
devolo-home-control-api==0.17.1
devolo-home-control-api==0.17.3
# homeassistant.components.directv
directv==0.4.0
@@ -721,7 +721,7 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
ha-philipsjs==2.3.2
ha-philipsjs==2.7.0
# homeassistant.components.habitica
habitipy==0.2.0
@@ -763,7 +763,7 @@ hole==0.5.1
holidays==0.10.5.2
# homeassistant.components.frontend
home-assistant-frontend==20210402.0
home-assistant-frontend==20210407.3
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -916,7 +916,7 @@ magicseaweed==1.0.3
matrix-client==0.3.2
# homeassistant.components.maxcube
maxcube-api==0.4.1
maxcube-api==0.4.2
# homeassistant.components.mythicbeastsdns
mbddns==0.1.2
@@ -986,7 +986,7 @@ netdisco==2.8.2
neurio==0.3.1
# homeassistant.components.nexia
nexia==0.9.5
nexia==0.9.6
# homeassistant.components.nextcloud
nextcloudmonitor==1.1.0
@@ -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
@@ -1476,7 +1476,7 @@ pykira==0.1.1
pykmtronic==0.0.3
# homeassistant.components.kodi
pykodi==0.2.3
pykodi==0.2.5
# homeassistant.components.kulersky
pykulersky==0.5.2
@@ -1822,7 +1822,7 @@ python-qbittorrent==0.4.2
python-ripple-api==0.0.3
# homeassistant.components.smarttub
python-smarttub==0.0.19
python-smarttub==0.0.23
# homeassistant.components.sochain
python-sochain-api==0.0.2
@@ -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
@@ -2372,7 +2372,7 @@ zengge==0.2
zeroconf==0.29.0
# homeassistant.components.zha
zha-quirks==0.0.55
zha-quirks==0.0.56
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -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
+17 -14
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
@@ -106,7 +106,7 @@ aioguardian==1.0.4
aioharmony==0.2.7
# homeassistant.components.homekit_controller
aiohomekit==0.2.60
aiohomekit==0.2.61
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -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
@@ -261,7 +261,7 @@ defusedxml==0.6.0
denonavr==0.9.10
# homeassistant.components.devolo_home_control
devolo-home-control-api==0.17.1
devolo-home-control-api==0.17.3
# homeassistant.components.directv
directv==0.4.0
@@ -382,7 +382,7 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
ha-philipsjs==2.3.2
ha-philipsjs==2.7.0
# homeassistant.components.habitica
habitipy==0.2.0
@@ -412,7 +412,7 @@ hole==0.5.1
holidays==0.10.5.2
# homeassistant.components.frontend
home-assistant-frontend==20210402.0
home-assistant-frontend==20210407.3
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -476,7 +476,7 @@ logi_circle==0.2.2
luftdaten==0.6.4
# homeassistant.components.maxcube
maxcube-api==0.4.1
maxcube-api==0.4.2
# homeassistant.components.mythicbeastsdns
mbddns==0.1.2
@@ -516,7 +516,10 @@ nessclient==0.9.15
netdisco==2.8.2
# homeassistant.components.nexia
nexia==0.9.5
nexia==0.9.6
# homeassistant.components.notify_events
notify-events==1.0.4
# homeassistant.components.nsw_fuel_station
nsw-fuel-api-client==1.0.10
@@ -690,7 +693,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
@@ -784,7 +787,7 @@ pykira==0.1.1
pykmtronic==0.0.3
# homeassistant.components.kodi
pykodi==0.2.3
pykodi==0.2.5
# homeassistant.components.kulersky
pykulersky==0.5.2
@@ -956,7 +959,7 @@ python-nest==4.1.0
python-openzwave-mqtt[mqtt-client]==1.4.0
# homeassistant.components.smarttub
python-smarttub==0.0.19
python-smarttub==0.0.23
# homeassistant.components.songpal
python-songpal==0.12
@@ -1095,7 +1098,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
@@ -1230,7 +1233,7 @@ zeep[async]==4.0.0
zeroconf==0.29.0
# homeassistant.components.zha
zha-quirks==0.0.55
zha-quirks==0.0.56
# homeassistant.components.zha
zigpy-cc==0.5.2
@@ -1251,4 +1254,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
+1 -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"
+56 -27
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"
+2 -8
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"
+244
View File
@@ -0,0 +1,244 @@
"""Tests for the Cast config flow."""
from unittest.mock import ANY, patch
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import cast
from tests.common import MockConfigEntry
async def test_creating_entry_sets_up_media_player(hass):
"""Test setting up Cast loads the media player."""
with patch(
"homeassistant.components.cast.media_player.async_setup_entry",
return_value=True,
) as mock_setup, patch(
"pychromecast.discovery.discover_chromecasts", return_value=(True, None)
), patch(
"pychromecast.discovery.stop_discovery"
):
result = await hass.config_entries.flow.async_init(
cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
# Confirmation form
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
@pytest.mark.parametrize("source", ["import", "user", "zeroconf"])
async def test_single_instance(hass, source):
"""Test we only allow a single config flow."""
MockConfigEntry(domain="cast").add_to_hass(hass)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
"cast", context={"source": source}
)
assert result["type"] == "abort"
assert result["reason"] == "single_instance_allowed"
async def test_user_setup(hass):
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
async def test_user_setup_options(hass):
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "}
)
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": ["192.168.0.1", "192.168.0.2"],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
async def test_zeroconf_setup(hass):
"""Test we can finish a config flow through zeroconf."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "zeroconf"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
def get_suggested(schema, key):
"""Get suggested value for key in voluptuous schema."""
for k in schema.keys():
if k == key:
if k.description is None or "suggested_value" not in k.description:
return None
return k.description["suggested_value"]
@pytest.mark.parametrize(
"parameter_data",
[
(
"known_hosts",
["192.168.0.10", "192.168.0.11"],
"192.168.0.10,192.168.0.11",
"192.168.0.1, , 192.168.0.2 ",
["192.168.0.1", "192.168.0.2"],
),
(
"uuid",
["bla", "blu"],
"bla,blu",
"foo, , bar ",
["foo", "bar"],
),
(
"ignore_cec",
["cast1", "cast2"],
"cast1,cast2",
"other_cast, , some_cast ",
["other_cast", "some_cast"],
),
],
)
async def test_option_flow(hass, parameter_data):
"""Test config flow options."""
all_parameters = ["ignore_cec", "known_hosts", "uuid"]
parameter, initial, suggested, user_input, updated = parameter_data
data = {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
}
data[parameter] = initial
config_entry = MockConfigEntry(domain="cast", data=data)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Test ignore_cec and uuid options are hidden if advanced options are disabled
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "options"
data_schema = result["data_schema"].schema
assert set(data_schema) == {"known_hosts"}
orig_data = dict(config_entry.data)
# Reconfigure ignore_cec, known_hosts, uuid
context = {"source": "user", "show_advanced_options": True}
result = await hass.config_entries.options.async_init(
config_entry.entry_id, context=context
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "options"
data_schema = result["data_schema"].schema
for other_param in all_parameters:
if other_param == parameter:
continue
assert get_suggested(data_schema, other_param) == ""
assert get_suggested(data_schema, parameter) == suggested
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={parameter: user_input},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] is None
for other_param in all_parameters:
if other_param == parameter:
continue
assert config_entry.data[other_param] == []
assert config_entry.data[parameter] == updated
# Clear known_hosts
result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"known_hosts": ""},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] is None
assert config_entry.data == {
**orig_data,
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
}
async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock):
"""Test known hosts is passed to pychromecasts."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"}
)
assert result["type"] == "create_entry"
await hass.async_block_till_done()
config_entry = hass.config_entries.async_entries("cast")[0]
assert castbrowser_mock.start_discovery.call_count == 1
castbrowser_constructor_mock.assert_called_once_with(
ANY, ANY, ["192.168.0.1", "192.168.0.2"]
)
castbrowser_mock.reset_mock()
castbrowser_constructor_mock.reset_mock()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"known_hosts": "192.168.0.11, 192.168.0.12"},
)
await hass.async_block_till_done()
castbrowser_mock.start_discovery.assert_not_called()
castbrowser_constructor_mock.assert_not_called()
castbrowser_mock.host_browser.update_hosts.assert_called_once_with(
["192.168.0.11", "192.168.0.12"]
)
+3 -237
View File
@@ -1,39 +1,9 @@
"""Tests for the Cast config flow."""
from unittest.mock import ANY, patch
"""Tests for the Cast integration."""
from unittest.mock import patch
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import cast
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
async def test_creating_entry_sets_up_media_player(hass):
"""Test setting up Cast loads the media player."""
with patch(
"homeassistant.components.cast.media_player.async_setup_entry",
return_value=True,
) as mock_setup, patch(
"pychromecast.discovery.discover_chromecasts", return_value=(True, None)
), patch(
"pychromecast.discovery.stop_discovery"
):
result = await hass.config_entries.flow.async_init(
cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
# Confirmation form
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
async def test_import(hass, caplog):
"""Test that specifying config will create an entry."""
@@ -67,7 +37,7 @@ async def test_import(hass, caplog):
async def test_not_configuring_cast_not_creates_entry(hass):
"""Test that no config will not create an entry."""
"""Test that an empty config does not create an entry."""
with patch(
"homeassistant.components.cast.async_setup_entry", return_value=True
) as mock_setup:
@@ -75,207 +45,3 @@ async def test_not_configuring_cast_not_creates_entry(hass):
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 0
@pytest.mark.parametrize("source", ["import", "user", "zeroconf"])
async def test_single_instance(hass, source):
"""Test we only allow a single config flow."""
MockConfigEntry(domain="cast").add_to_hass(hass)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
"cast", context={"source": source}
)
assert result["type"] == "abort"
assert result["reason"] == "single_instance_allowed"
async def test_user_setup(hass):
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
async def test_user_setup_options(hass):
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "}
)
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": ["192.168.0.1", "192.168.0.2"],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
async def test_zeroconf_setup(hass):
"""Test we can finish a config flow through zeroconf."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "zeroconf"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
def get_suggested(schema, key):
"""Get suggested value for key in voluptuous schema."""
for k in schema.keys():
if k == key:
if k.description is None or "suggested_value" not in k.description:
return None
return k.description["suggested_value"]
@pytest.mark.parametrize(
"parameter_data",
[
(
"known_hosts",
["192.168.0.10", "192.168.0.11"],
"192.168.0.10,192.168.0.11",
"192.168.0.1, , 192.168.0.2 ",
["192.168.0.1", "192.168.0.2"],
),
(
"uuid",
["bla", "blu"],
"bla,blu",
"foo, , bar ",
["foo", "bar"],
),
(
"ignore_cec",
["cast1", "cast2"],
"cast1,cast2",
"other_cast, , some_cast ",
["other_cast", "some_cast"],
),
],
)
async def test_option_flow(hass, parameter_data):
"""Test config flow options."""
all_parameters = ["ignore_cec", "known_hosts", "uuid"]
parameter, initial, suggested, user_input, updated = parameter_data
data = {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
}
data[parameter] = initial
config_entry = MockConfigEntry(domain="cast", data=data)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Test ignore_cec and uuid options are hidden if advanced options are disabled
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "options"
data_schema = result["data_schema"].schema
assert set(data_schema) == {"known_hosts"}
# Reconfigure ignore_cec, known_hosts, uuid
context = {"source": "user", "show_advanced_options": True}
result = await hass.config_entries.options.async_init(
config_entry.entry_id, context=context
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "options"
data_schema = result["data_schema"].schema
for other_param in all_parameters:
if other_param == parameter:
continue
assert get_suggested(data_schema, other_param) == ""
assert get_suggested(data_schema, parameter) == suggested
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={parameter: user_input},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] is None
for other_param in all_parameters:
if other_param == parameter:
continue
assert config_entry.data[other_param] == []
assert config_entry.data[parameter] == updated
# Clear known_hosts
result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"known_hosts": ""},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] is None
assert config_entry.data == {"ignore_cec": [], "known_hosts": [], "uuid": []}
async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock):
"""Test known hosts is passed to pychromecasts."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"}
)
assert result["type"] == "create_entry"
await hass.async_block_till_done()
config_entry = hass.config_entries.async_entries("cast")[0]
assert castbrowser_mock.start_discovery.call_count == 1
castbrowser_constructor_mock.assert_called_once_with(
ANY, ANY, ["192.168.0.1", "192.168.0.2"]
)
castbrowser_mock.reset_mock()
castbrowser_constructor_mock.reset_mock()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"known_hosts": "192.168.0.11, 192.168.0.12"},
)
await hass.async_block_till_done()
castbrowser_mock.start_discovery.assert_not_called()
castbrowser_constructor_mock.assert_not_called()
castbrowser_mock.host_browser.update_hosts.assert_called_once_with(
["192.168.0.11", "192.168.0.12"]
)
+83 -34
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,
@@ -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):
+1 -1
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()
+2
View File
@@ -42,6 +42,7 @@ async def test_log_filtering(hass, caplog):
"doesntmatchanything",
".*shouldfilterall.*",
"^filterthis:.*",
"in the middle",
],
"test.other_filter": [".*otherfilterer"],
},
@@ -62,6 +63,7 @@ async def test_log_filtering(hass, caplog):
filter_logger, False, "this line containing shouldfilterall should be filtered"
)
msg_test(filter_logger, True, "this line should not be filtered filterthis:")
msg_test(filter_logger, False, "this in the middle should be filtered")
msg_test(filter_logger, False, "filterthis: should be filtered")
msg_test(filter_logger, False, "format string shouldfilter%s", "all")
msg_test(filter_logger, True, "format string shouldfilter%s", "not")
+2 -1
View File
@@ -10,6 +10,7 @@ import pytest
from homeassistant.components.maxcube import DOMAIN
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import now
@pytest.fixture
@@ -105,5 +106,5 @@ async def cube(hass, hass_config, room, thermostat, wallthermostat, windowshutte
assert await async_setup_component(hass, DOMAIN, hass_config)
await hass.async_block_till_done()
gateway = hass_config[DOMAIN]["gateways"][0]
mock.assert_called_with(gateway["host"], gateway.get("port", 62910))
mock.assert_called_with(gateway["host"], gateway.get("port", 62910), now=now)
return cube
+7 -5
View File
@@ -1,5 +1,6 @@
"""Test Media Source initialization."""
from unittest.mock import patch
from urllib.parse import quote
import pytest
@@ -45,7 +46,7 @@ async def test_async_browse_media(hass):
media = await media_source.async_browse_media(hass, "")
assert isinstance(media, media_source.models.BrowseMediaSource)
assert media.title == "media/"
assert len(media.children) == 1
assert len(media.children) == 2
# Test invalid media content
with pytest.raises(ValueError):
@@ -133,14 +134,15 @@ async def test_websocket_browse_media(hass, hass_ws_client):
assert msg["error"]["message"] == "test"
async def test_websocket_resolve_media(hass, hass_ws_client):
@pytest.mark.parametrize("filename", ["test.mp3", "Epic Sax Guy 10 Hours.mp4"])
async def test_websocket_resolve_media(hass, hass_ws_client, filename):
"""Test browse media websocket."""
assert await async_setup_component(hass, const.DOMAIN, {})
await hass.async_block_till_done()
client = await hass_ws_client(hass)
media = media_source.models.PlayMedia("/media/local/test.mp3", "audio/mpeg")
media = media_source.models.PlayMedia(f"/media/local/{filename}", "audio/mpeg")
with patch(
"homeassistant.components.media_source.async_resolve_media",
@@ -150,7 +152,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client):
{
"id": 1,
"type": "media_source/resolve_media",
"media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/test.mp3",
"media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/{filename}",
}
)
@@ -158,7 +160,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client):
assert msg["success"]
assert msg["id"] == 1
assert msg["result"]["url"].startswith(media.url)
assert msg["result"]["url"].startswith(quote(media.url))
assert msg["result"]["mime_type"] == media.mime_type
with patch(
@@ -95,5 +95,8 @@ async def test_media_view(hass, hass_client):
resp = await client.get("/media/local/test.mp3")
assert resp.status == 200
resp = await client.get("/media/local/Epic Sax Guy 10 Hours.mp4")
assert resp.status == 200
resp = await client.get("/media/recordings/test.mp3")
assert resp.status == 200
+8 -4
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",
+20
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 = {
+32 -2
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
)

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