Compare commits

..

77 Commits

Author SHA1 Message Date
Paulus Schoutsen
fc2d30c993 Merge pull request #66351 from home-assistant/rc 2022-02-11 14:11:43 -08:00
Paulus Schoutsen
cb7f7dff72 Bumped version to 2022.2.6 2022-02-11 13:31:16 -08:00
J. Nick Koston
c254598331 Add unique id to lutron caseta config entry when missing (#66346) 2022-02-11 13:30:42 -08:00
Franck Nijhof
646c56e0e9 Fix CPUSpeed with missing info (#66339) 2022-02-11 13:30:42 -08:00
Franck Nijhof
f3a3ff28f2 Fix PVOutput when no data is available (#66338) 2022-02-11 13:30:41 -08:00
Allen Porter
087f443368 Fix nest streams that get stuck broken (#66334) 2022-02-11 13:30:40 -08:00
epenet
fcee1ff865 Fix raspihats initialization (#66330)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-02-11 13:30:39 -08:00
J. Nick Koston
6084b323df Reduce number of parallel api calls to august (#66328) 2022-02-11 13:30:39 -08:00
starkillerOG
6857562e9e bump motionblinds to 0.5.12 (#66323) 2022-02-11 13:30:38 -08:00
starkillerOG
aef2588f9c bump motionblinds to 0.5.11 (#65988) 2022-02-11 13:30:37 -08:00
Joakim Sørensen
27c5460feb Add guard for invalid EntityCategory value (#66316) 2022-02-11 13:29:07 -08:00
Allen Porter
60b4600019 Bump google-nest-sdm to 1.7.1 (minor patch) (#66304) 2022-02-11 13:26:51 -08:00
Allen Porter
27752f7ad3 Bump google-nest-sdm to 1.7.0 (#66145) 2022-02-11 13:26:51 -08:00
Allen Porter
669c99474b Bump python-nest to 4.2.0 for python 3.10 fixes (#66090) 2022-02-11 13:26:50 -08:00
J. Nick Koston
76872e3789 Fix august token refresh when data contains characters outside of latin1 (#66303)
* WIP

* bump version

* bump
2022-02-11 13:25:01 -08:00
Joakim Plate
2594500452 Correct philips_js usage of the overloaded coordinator (#66287) 2022-02-11 13:25:00 -08:00
uvjustin
65c8363323 Catch ConnectionResetError when writing MJPEG in camera (#66245) 2022-02-11 13:24:59 -08:00
ufodone
dfcad3a13d Disable zone bypass switch feature (#66243)
* Add configuration option to disable the creation of zone bypass switches

* Removed temporary workaround and bumped pyenvisalink version to pick up the correct fix.

* Remove zone bypass configuration option and disable zone bypass switches per code review instructions.
2022-02-11 13:24:35 -08:00
Milan Meulemans
92bc780dd7 Bump aioaseko to 0.0.2 to fix issue (#66240) 2022-02-11 13:22:13 -08:00
Michael
eb781060e8 bump py-synologydsm-api to 1.0.6 (#66226) 2022-02-11 13:22:13 -08:00
Maximilian
a2e7897b1e Add missing nina warnings (#66211) 2022-02-11 13:22:12 -08:00
jjlawren
854308fec2 Handle more Sonos favorites in media browser (#66205) 2022-02-11 13:22:11 -08:00
Otto Winter
0199e8cc43 Bump aioesphomeapi from 10.8.1 to 10.8.2 (#66189) 2022-02-11 13:22:11 -08:00
Erik Montnemery
7cc9a4310d Fix controlling nested groups (#66176) 2022-02-11 13:22:10 -08:00
epenet
5976238126 Fix hdmi-cec initialization (#66172) 2022-02-11 13:22:09 -08:00
Franck Nijhof
caedef5f1a Reduce Spotify API usage (#66315) 2022-02-11 13:16:41 -08:00
Franck Nijhof
a96b91d120 Merge pull request #66161 from home-assistant/rc 2022-02-09 11:47:23 +01:00
Franck Nijhof
2e6ee5165e Bumped version to 2022.2.5 2022-02-09 10:56:05 +01:00
Erik Montnemery
7dd7c1dadd Fix MQTT debug info (#66146) 2022-02-09 10:55:26 +01:00
Aaron Bach
4c548af6ef Bump simplisafe-python to 2022.02.1 (#66140) 2022-02-09 10:55:23 +01:00
Michael
200e07b8d6 Fix system is loaded flag during reboot/shutdown of Synology DSM (#66125) 2022-02-09 10:55:20 +01:00
Richard Benson
ae5a885387 Bump amcrest to 1.9.4 (#66124)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-02-09 10:55:17 +01:00
Simone Chemelli
bebdaacf47 Change detection of router devices for Fritz (#65965) 2022-02-09 10:55:13 +01:00
Erik Montnemery
339fc0a2af Fix flaky homewizard test (#65490) 2022-02-09 10:55:09 +01:00
Dave T
f44ca5f9d5 Fix generic camera typo in attr_frame_interval (#65390) 2022-02-09 10:55:04 +01:00
Paulus Schoutsen
a869c1bc88 Merge pull request #66103 from home-assistant/rc 2022-02-08 14:48:49 -08:00
Erik Montnemery
d5443b8dee Fix ENTITY_CATEGORIES_SCHEMA (#66108)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-08 14:07:59 -08:00
Erik Montnemery
6ec09320dd Fix cleanup of MQTT debug info (#66104) 2022-02-08 14:07:43 -08:00
Paulus Schoutsen
550f80ddd2 Bumped version to 2022.2.4 2022-02-08 12:03:54 -08:00
Raman Gupta
23d2168952 Fix schema for zwave_js WS API (#66052) 2022-02-08 12:03:42 -08:00
J. Nick Koston
c1cb0a0f8e Fix missing exception catch in august to prevent failed setup (#66045) 2022-02-08 12:03:41 -08:00
Erik Montnemery
e53227be79 Fix race in MQTT sensor and binary_sensor expire_after (#66040) 2022-02-08 12:03:40 -08:00
J. Nick Koston
c8c1543b26 Fix decoding discovery with old Magic Home firmwares (#66038) 2022-02-08 12:03:39 -08:00
jjlawren
715fe95abd Clean up Sonos unsubscribe/resubscribe exception handling and logging (#66025) 2022-02-08 12:03:38 -08:00
Erik Montnemery
02cb879717 Speed up deletion of duplicated statistics (#66014) 2022-02-08 12:03:37 -08:00
Joakim Sørensen
9734216215 Use strings directly instead of Enums in version config (#66007) 2022-02-08 12:03:37 -08:00
Erik Montnemery
0f06ebde06 Revert "Make idle chromecasts appear as idle instead of off" (#66005) 2022-02-08 12:03:35 -08:00
Erik Montnemery
7195372616 Suppress unwanted error messages during recorder migration (#66004) 2022-02-08 12:03:35 -08:00
Joakim Sørensen
ac63a7e01e Add diagnostics to Version integration (#65999)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-02-08 12:03:34 -08:00
jjlawren
f08ebf5b7e Bump plexapi to 4.9.2 (#65972) 2022-02-08 12:03:33 -08:00
Tiernan
49d6048278 Fix TOD incorrectly determining the state between sunrise and sunset (#65884)
* Fix TOD component incorrectly determining the state between sunrise and sunset (#30199)

* TOD fix

* Comment added

* Review

* Review

* Review

* Update time after day fix workaround for compatibility with
current version.
Only apply fix when using times and not when using sun events.
Add unit test for behaviour.

Co-authored-by: Nikolay Vasilchuk <Anonym.tsk@gmail.com>
2022-02-08 12:03:32 -08:00
Paulus Schoutsen
ceae63d457 Fix UPNP access to SSDP info (#65728) 2022-02-08 12:03:31 -08:00
Paulus Schoutsen
f170aba0cc Merge pull request #65955 from home-assistant/rc 2022-02-06 15:30:01 -08:00
M. Frister
66e076b57f Bump soco to 0.26.2 (#65919) 2022-02-06 14:35:14 -08:00
Paulus Schoutsen
1338b347b5 Remove duplicate methods 2022-02-06 14:33:07 -08:00
Paulus Schoutsen
9b471ab653 Bumped version to 2022.2.3 2022-02-06 14:23:08 -08:00
J. Nick Koston
e90a6bbe1c Add diagnostics support to HomeKit (#65942)
* Add diagnostics support to HomeKit

* remove debug
2022-02-06 14:23:03 -08:00
Michael
aa9965675d Improve device shutdown and unload of Synology DSM integration (#65936)
* ignore errors during unload/logout

* automatic host update is an info, nut debug
2022-02-06 14:23:02 -08:00
Joakim Sørensen
ad3b2f02b4 disabled_by can be None when updating devices (#65934) 2022-02-06 14:23:02 -08:00
Matthias Alphart
0dbe9b7cf4 Update xknx to 0.19.2 - fix TCP tunnelling (#65920) 2022-02-06 14:23:01 -08:00
J. Nick Koston
b9d346baed Fix loss of ability to control white channel in HomeKit on RGB&W lights (#65864)
* Fix loss of ability to control white channel in HomeKit on RGB&W lights

- Fix white channel missing from RGB/W lights

- Fix temp missing from RGB/CW lights

- Fixes #65529

* cover the missing case

* bright fix

* force brightness notify on color mode change as well
2022-02-06 14:23:00 -08:00
Jeef
7791711603 feat: bumped version (#65863) 2022-02-06 14:22:59 -08:00
jjlawren
fdfffcb73e Fix Spotify, Tidal, Apple Music playback on Sonos groups (#65838) 2022-02-06 14:22:59 -08:00
J. Nick Koston
8e6bd840a4 Fix flash at turn on with newer 0x04 Magic Home models (#65836) 2022-02-06 14:22:58 -08:00
Allen Porter
619a52a387 Fix legacy nest diagnostics to return empty rather than fail (#65824)
Fix legacy nest diangostics to return gracefully, rather than a TypError
by checking explicitiy for SDM in the config entry. Update diagnostics
to use the common nest test fixture, and extend with support for the
legacy nest config. Use the sdm test fixture in the existing legacy
tests so they all share the same config files.
2022-02-06 14:22:57 -08:00
Shay Levy
a4d59aa599 Bump aioshelly to 1.0.9 (#65803) 2022-02-06 14:22:57 -08:00
Ferdinand
4ba494f5cd Fix the restart when the saj device is down (#65796) 2022-02-06 14:22:56 -08:00
Franck Nijhof
7a7f9deb89 Update Pillow to 9.0.1 (#65779) 2022-02-06 14:19:47 -08:00
J. Nick Koston
5786f68bb7 Prevent multiple dhcp flows from being started for the same device/domain (#65753) 2022-02-06 14:19:46 -08:00
Aaron Bach
bccfe6646e Add redacted subscription data to SimpliSafe diagnostics (#65751) 2022-02-06 14:19:45 -08:00
ollo69
fc7ea6e1b3 Improve androidtv mac address handling and test coverage (#65749)
* Better mac addr handling and improve test coverage

* Apply suggested changes

* Apply more suggested changes
2022-02-06 14:19:44 -08:00
Aaron Bach
058420bb2f Bump simplisafe-python to 2022.02.0 (#65748) 2022-02-06 14:19:44 -08:00
Maciej Bieniek
9695235920 Fix wind speed unit (#65723) 2022-02-06 14:19:43 -08:00
J. Nick Koston
57526bd21f Add coverage for color_rgbww_to_rgb, fix divzero case (#65721) 2022-02-06 14:19:42 -08:00
Sean Vig
eff9690c8a Fix Amcrest service calls (#65717)
Fixes #65522
Fixes #65647
2022-02-06 14:19:42 -08:00
Aidan Timson
d754ea1645 Fix OVO Energy NoneType error occurring for some users (#65714) 2022-02-06 14:19:41 -08:00
Michael
5f6214ede7 check wan access type (#65389) 2022-02-06 14:19:40 -08:00
123 changed files with 2084 additions and 637 deletions

View File

@@ -264,6 +264,7 @@ tests/components/enphase_envoy/* @gtdiehl
homeassistant/components/entur_public_transport/* @hfurubotten
homeassistant/components/environment_canada/* @gwww @michaeldavie
tests/components/environment_canada/* @gwww @michaeldavie
homeassistant/components/envisalink/* @ufodone
homeassistant/components/ephember/* @ttroy50
homeassistant/components/epson/* @pszafer
tests/components/epson/* @pszafer

View File

@@ -17,7 +17,12 @@ from homeassistant.components.weather import (
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.const import (
CONF_NAME,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
@@ -62,9 +67,13 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
"""Initialize."""
super().__init__(coordinator)
self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL
self._attr_wind_speed_unit = self.coordinator.data["Wind"]["Speed"][
self._unit_system
]["Unit"]
wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][
"Unit"
]
if wind_speed_unit == "mi/h":
self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR
else:
self._attr_wind_speed_unit = wind_speed_unit
self._attr_name = name
self._attr_unique_id = coordinator.location_key
self._attr_temperature_unit = (

View File

@@ -515,8 +515,8 @@ class AmcrestCam(Camera):
max_tries = 3
for tries in range(max_tries, 0, -1):
try:
await getattr(self, f"_set_{func}")(value)
new_value = await getattr(self, f"_get_{func}")()
await getattr(self, f"_async_set_{func}")(value)
new_value = await getattr(self, f"_async_get_{func}")()
if new_value != value:
raise AmcrestCommandFailed
except (AmcrestError, AmcrestCommandFailed) as error:

View File

@@ -2,7 +2,7 @@
"domain": "amcrest",
"name": "Amcrest",
"documentation": "https://www.home-assistant.io/integrations/amcrest",
"requirements": ["amcrest==1.9.3"],
"requirements": ["amcrest==1.9.4"],
"dependencies": ["ffmpeg"],
"codeowners": ["@flacjacket"],
"iot_class": "local_polling"

View File

@@ -18,6 +18,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType
@@ -33,16 +34,30 @@ from .const import (
DEVICE_ANDROIDTV,
DEVICE_FIRETV,
DOMAIN,
PROP_ETHMAC,
PROP_SERIALNO,
PROP_WIFIMAC,
SIGNAL_CONFIG_ENTITY,
)
PLATFORMS = [Platform.MEDIA_PLAYER]
RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
_LOGGER = logging.getLogger(__name__)
def get_androidtv_mac(dev_props):
"""Return formatted mac from device properties."""
for prop_mac in (PROP_ETHMAC, PROP_WIFIMAC):
if if_mac := dev_props.get(prop_mac):
mac = format_mac(if_mac)
if mac not in _INVALID_MACS:
return mac
return None
def _setup_androidtv(hass, config):
"""Generate an ADB key (if needed) and load it."""
adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey"))

View File

@@ -11,9 +11,8 @@ from homeassistant import config_entries
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from . import async_connect_androidtv
from . import async_connect_androidtv, get_androidtv_mac
from .const import (
CONF_ADB_SERVER_IP,
CONF_ADB_SERVER_PORT,
@@ -132,9 +131,7 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
PROP_WIFIMAC,
dev_prop.get(PROP_WIFIMAC),
)
unique_id = format_mac(
dev_prop.get(PROP_ETHMAC) or dev_prop.get(PROP_WIFIMAC, "")
)
unique_id = get_androidtv_mac(dev_prop)
await aftv.adb_close()
return None, unique_id

View File

@@ -51,12 +51,13 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import get_androidtv_mac
from .const import (
ANDROID_DEV,
ANDROID_DEV_OPT,
@@ -80,8 +81,6 @@ from .const import (
DEVICE_ANDROIDTV,
DEVICE_CLASSES,
DOMAIN,
PROP_ETHMAC,
PROP_WIFIMAC,
SIGNAL_CONFIG_ENTITY,
)
@@ -343,7 +342,7 @@ class ADBDevice(MediaPlayerEntity):
self._attr_device_info[ATTR_MANUFACTURER] = manufacturer
if sw_version := info.get(ATTR_SW_VERSION):
self._attr_device_info[ATTR_SW_VERSION] = sw_version
if mac := format_mac(info.get(PROP_ETHMAC) or info.get(PROP_WIFIMAC, "")):
if mac := get_androidtv_mac(info):
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)}
self._app_id_to_name = {}

View File

@@ -3,7 +3,7 @@
"name": "Aseko Pool Live",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
"requirements": ["aioaseko==0.0.1"],
"requirements": ["aioaseko==0.0.2"],
"codeowners": [
"@milanmeu"
],

View File

@@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryAuthFailed from err
except asyncio.TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from err
except (ClientResponseError, CannotConnect) as err:
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
raise ConfigEntryNotReady from err
@@ -75,6 +75,7 @@ async def async_setup_august(
hass.config_entries.async_update_entry(config_entry, data=config_data)
await august_gateway.async_authenticate()
await august_gateway.async_refresh_access_token_if_needed()
hass.data.setdefault(DOMAIN, {})
data = hass.data[DOMAIN][config_entry.entry_id] = {
@@ -106,11 +107,10 @@ class AugustData(AugustSubscriberMixin):
async def async_setup(self):
"""Async setup of august device data and activities."""
token = self._august_gateway.access_token
user_data, locks, doorbells = await asyncio.gather(
self._api.async_get_user(token),
self._api.async_get_operable_locks(token),
self._api.async_get_doorbells(token),
)
# This used to be a gather but it was less reliable with august's recent api changes.
user_data = await self._api.async_get_user(token)
locks = await self._api.async_get_operable_locks(token)
doorbells = await self._api.async_get_doorbells(token)
if not doorbells:
doorbells = []
if not locks:

View File

@@ -2,7 +2,7 @@
"domain": "august",
"name": "August",
"documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["yalexs==1.1.20"],
"requirements": ["yalexs==1.1.22"],
"codeowners": ["@bdraco"],
"dhcp": [
{

View File

@@ -222,7 +222,12 @@ async def async_get_mjpeg_stream(
"""Fetch an mjpeg stream from a camera entity."""
camera = _get_camera_from_entity_id(hass, entity_id)
return await camera.handle_async_mjpeg_stream(request)
try:
stream = await camera.handle_async_mjpeg_stream(request)
except ConnectionResetError:
stream = None
_LOGGER.debug("Error while writing MJPEG stream to transport")
return stream
async def async_get_still_stream(
@@ -784,7 +789,11 @@ class CameraMjpegStream(CameraView):
async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse:
"""Serve camera stream, possibly with interval."""
if (interval_str := request.query.get("interval")) is None:
stream = await camera.handle_async_mjpeg_stream(request)
try:
stream = await camera.handle_async_mjpeg_stream(request)
except ConnectionResetError:
stream = None
_LOGGER.debug("Error while writing MJPEG stream to transport")
if stream is None:
raise web.HTTPBadGateway()
return stream

View File

@@ -50,6 +50,7 @@ from homeassistant.const import (
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
EVENT_HOMEASSISTANT_STOP,
STATE_IDLE,
STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
)
@@ -636,7 +637,7 @@ class CastDevice(MediaPlayerEntity):
return STATE_PLAYING
return STATE_IDLE
if self._chromecast is not None and self._chromecast.is_idle:
return STATE_IDLE
return STATE_OFF
return None
@property

View File

@@ -62,7 +62,7 @@ async def websocket_update_device(hass, connection, msg):
msg.pop("type")
msg_id = msg.pop("id")
if "disabled_by" in msg:
if msg.get("disabled_by") is not None:
msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"])
entry = registry.async_update_device(**msg)

View File

@@ -81,8 +81,8 @@ class CPUSpeedSensor(SensorEntity):
if info:
self._attr_extra_state_attributes = {
ATTR_ARCH: info["arch_string_raw"],
ATTR_BRAND: info["brand_raw"],
ATTR_ARCH: info.get("arch_string_raw"),
ATTR_BRAND: info.get("brand_raw"),
}
if HZ_ADVERTISED in info:
self._attr_extra_state_attributes[ATTR_HZ] = round(

View File

@@ -179,6 +179,7 @@ class WatcherBase:
lowercase_hostname,
)
matched_domains = set()
for entry in self._integration_matchers:
if MAC_ADDRESS in entry and not fnmatch.fnmatch(
uppercase_mac, entry[MAC_ADDRESS]
@@ -191,6 +192,11 @@ class WatcherBase:
continue
_LOGGER.debug("Matched %s against %s", data, entry)
if entry["domain"] in matched_domains:
# Only match once per domain
continue
matched_domains.add(entry["domain"])
discovery_flow.async_create_flow(
self.hass,
entry["domain"],

View File

@@ -2,7 +2,7 @@
"domain": "doods",
"name": "DOODS - Dedicated Open Object Detection Service",
"documentation": "https://www.home-assistant.io/integrations/doods",
"requirements": ["pydoods==1.0.2", "pillow==9.0.0"],
"requirements": ["pydoods==1.0.2", "pillow==9.0.1"],
"codeowners": [],
"iot_class": "local_polling"
}

View File

@@ -137,6 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
keep_alive,
hass.loop,
connection_timeout,
False,
)
hass.data[DATA_EVL] = controller
@@ -181,12 +182,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
_LOGGER.debug("The envisalink sent a partition update event")
async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data)
@callback
def async_zone_bypass_update(data):
"""Handle zone bypass status updates."""
_LOGGER.debug("Envisalink sent a zone bypass update event. Updating zones")
async_dispatcher_send(hass, SIGNAL_ZONE_BYPASS_UPDATE, data)
@callback
def stop_envisalink(event):
"""Shutdown envisalink connection and thread on exit."""
@@ -206,7 +201,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
controller.callback_login_failure = async_login_fail_callback
controller.callback_login_timeout = async_connection_fail_callback
controller.callback_login_success = async_connection_success_callback
controller.callback_zone_bypass_update = async_zone_bypass_update
_LOGGER.info("Start envisalink")
controller.start()
@@ -240,13 +234,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, Platform.BINARY_SENSOR, "envisalink", {CONF_ZONES: zones}, config
)
)
# Only DSC panels support getting zone bypass status
if panel_type == PANEL_TYPE_DSC:
hass.async_create_task(
async_load_platform(
hass, "switch", "envisalink", {CONF_ZONES: zones}, config
)
)
# Zone bypass switches are not currently created due to an issue with some panels.
# These switches will be re-added in the future after some further refactoring of the integration.
hass.services.async_register(
DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA

View File

@@ -2,7 +2,7 @@
"domain": "envisalink",
"name": "Envisalink",
"documentation": "https://www.home-assistant.io/integrations/envisalink",
"requirements": ["pyenvisalink==4.3"],
"codeowners": [],
"requirements": ["pyenvisalink==4.4"],
"codeowners": ["@ufodone"],
"iot_class": "local_push"
}

View File

@@ -3,7 +3,7 @@
"name": "ESPHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome",
"requirements": ["aioesphomeapi==10.8.1"],
"requirements": ["aioesphomeapi==10.8.2"],
"zeroconf": ["_esphomelib._tcp.local."],
"codeowners": ["@OttoWinter", "@jesserockz"],
"after_dependencies": ["zeroconf", "tag"],

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.28.20"],
"requirements": ["flux_led==0.28.22"],
"quality_scale": "platinum",
"codeowners": ["@icemanch", "@bdraco"],
"iot_class": "local_push",

View File

@@ -155,7 +155,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
self.hass = hass
self.host = host
self.mesh_role = MeshRoles.NONE
self.device_is_router: bool = True
self.device_conn_type: str | None = None
self.device_is_router: bool = False
self.password = password
self.port = port
self.username = username
@@ -213,7 +214,15 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
self._current_firmware = info.get("NewSoftwareVersion")
self._update_available, self._latest_firmware = self._update_device_info()
self.device_is_router = "WANIPConn1" in self.connection.services
if "Layer3Forwarding1" in self.connection.services:
if connection_type := self.connection.call_action(
"Layer3Forwarding1", "GetDefaultConnectionService"
).get("NewDefaultConnectionService"):
# Return NewDefaultConnectionService sample: "1.WANPPPConnection.1"
self.device_conn_type = connection_type[2:][:-2]
self.device_is_router = self.connection.call_action(
self.device_conn_type, "GetInfo"
).get("NewEnable")
@callback
async def _async_update_data(self) -> None:
@@ -567,13 +576,6 @@ class AvmWrapper(FritzBoxTools):
)
return {}
async def async_get_wan_dsl_interface_config(self) -> dict[str, Any]:
"""Call WANDSLInterfaceConfig service."""
return await self.hass.async_add_executor_job(
partial(self.get_wan_dsl_interface_config)
)
async def async_get_wan_link_properties(self) -> dict[str, Any]:
"""Call WANCommonInterfaceConfig service."""
@@ -678,11 +680,6 @@ class AvmWrapper(FritzBoxTools):
return self._service_call_action("WLANConfiguration", str(index), "GetInfo")
def get_wan_dsl_interface_config(self) -> dict[str, Any]:
"""Call WANDSLInterfaceConfig service."""
return self._service_call_action("WANDSLInterfaceConfig", "1", "GetInfo")
def get_wan_link_properties(self) -> dict[str, Any]:
"""Call WANCommonInterfaceConfig service."""

View File

@@ -25,6 +25,7 @@ async def async_get_config_entry_diagnostics(
"current_firmware": avm_wrapper.current_firmware,
"latest_firmware": avm_wrapper.latest_firmware,
"update_available": avm_wrapper.update_available,
"connection_type": avm_wrapper.device_conn_type,
"is_router": avm_wrapper.device_is_router,
"mesh_role": avm_wrapper.mesh_role,
"last_update success": avm_wrapper.last_update_success,

View File

@@ -277,10 +277,14 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box sensors")
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
dsl: bool = False
dslinterface = await avm_wrapper.async_get_wan_dsl_interface_config()
if dslinterface:
dsl = dslinterface["NewEnable"]
link_properties = await avm_wrapper.async_get_wan_link_properties()
dsl: bool = link_properties.get("NewWANAccessType") == "DSL"
_LOGGER.debug(
"WANAccessType of FritzBox %s is '%s'",
avm_wrapper.host,
link_properties.get("NewWANAccessType"),
)
entities = [
FritzBoxSensor(avm_wrapper, entry.title, description)

View File

@@ -81,16 +81,12 @@ def port_entities_list(
_LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PORTFORWARD)
entities_list: list[FritzBoxPortSwitch] = []
connection_type = avm_wrapper.get_default_connection()
if not connection_type:
if not avm_wrapper.device_conn_type:
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_PORTFORWARD)
return []
# Return NewDefaultConnectionService sample: "1.WANPPPConnection.1"
con_type: str = connection_type["NewDefaultConnectionService"][2:][:-2]
# Query port forwardings and setup a switch for each forward for the current device
resp = avm_wrapper.get_num_port_mapping(con_type)
resp = avm_wrapper.get_num_port_mapping(avm_wrapper.device_conn_type)
if not resp:
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
return []
@@ -107,7 +103,7 @@ def port_entities_list(
for i in range(port_forwards_count):
portmap = avm_wrapper.get_port_mapping(con_type, i)
portmap = avm_wrapper.get_port_mapping(avm_wrapper.device_conn_type, i)
if not portmap:
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
continue
@@ -133,7 +129,7 @@ def port_entities_list(
portmap,
port_name,
i,
con_type,
avm_wrapper.device_conn_type,
)
)

View File

@@ -96,7 +96,7 @@ class GenericCamera(Camera):
if self._stream_source is not None:
self._stream_source.hass = hass
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
self._attr_frames_interval = 1 / device_info[CONF_FRAMERATE]
self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE]
self._supported_features = SUPPORT_STREAM if self._stream_source else 0
self.content_type = device_info[CONF_CONTENT_TYPE]
self.verify_ssl = device_info[CONF_VERIFY_SSL]

View File

@@ -57,6 +57,8 @@ KEY_POSITION = "position"
DEFAULT_NAME = "Cover Group"
# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{

View File

@@ -52,6 +52,8 @@ SUPPORTED_FLAGS = {SUPPORT_SET_SPEED, SUPPORT_DIRECTION, SUPPORT_OSCILLATE}
DEFAULT_NAME = "Fan Group"
# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{

View File

@@ -58,6 +58,9 @@ from .util import find_state_attributes, mean_tuple, reduce_attribute
DEFAULT_NAME = "Light Group"
# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,

View File

@@ -191,6 +191,8 @@ def parse_mapping(mapping, parents=None):
def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901
"""Set up the CEC capability."""
hass.data[DOMAIN] = {}
# Parse configuration into a dict of device name to physical address
# represented as a list of four elements.
device_aliases = {}

View File

@@ -0,0 +1,44 @@
"""Diagnostics support for HomeKit."""
from __future__ import annotations
from typing import Any
from pyhap.accessory_driver import AccessoryDriver
from pyhap.state import State
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import HomeKit
from .const import DOMAIN, HOMEKIT
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
homekit: HomeKit = hass.data[DOMAIN][entry.entry_id][HOMEKIT]
driver: AccessoryDriver = homekit.driver
data: dict[str, Any] = {
"status": homekit.status,
"config-entry": {
"title": entry.title,
"version": entry.version,
"data": dict(entry.data),
"options": dict(entry.options),
},
}
if not driver:
return data
data.update(driver.get_accessories())
state: State = driver.state
data.update(
{
"client_properties": {
str(client): props for client, props in state.client_properties.items()
},
"config_version": state.config_version,
"pairing_id": state.mac,
}
)
return data

View File

@@ -1,4 +1,6 @@
"""Class to hold all light accessories."""
from __future__ import annotations
import logging
import math
@@ -12,12 +14,13 @@ from homeassistant.components.light import (
ATTR_HS_COLOR,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
ATTR_WHITE,
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_WHITE,
DOMAIN,
brightness_supported,
color_supported,
@@ -32,9 +35,9 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later
from homeassistant.util.color import (
color_hsv_to_RGB,
color_temperature_mired_to_kelvin,
color_temperature_to_hs,
color_temperature_to_rgbww,
)
from .accessories import TYPES, HomeAccessory
@@ -51,12 +54,13 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
RGB_COLOR = "rgb_color"
CHANGE_COALESCE_TIME_WINDOW = 0.01
DEFAULT_MIN_MIREDS = 153
DEFAULT_MAX_MIREDS = 500
COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW}
COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE}
@TYPES.register("Light")
@@ -79,8 +83,12 @@ class Light(HomeAccessory):
self.color_modes = color_modes = (
attributes.get(ATTR_SUPPORTED_COLOR_MODES) or []
)
self._previous_color_mode = attributes.get(ATTR_COLOR_MODE)
self.color_supported = color_supported(color_modes)
self.color_temp_supported = color_temp_supported(color_modes)
self.rgbw_supported = COLOR_MODE_RGBW in color_modes
self.rgbww_supported = COLOR_MODE_RGBWW in color_modes
self.white_supported = COLOR_MODE_WHITE in color_modes
self.brightness_supported = brightness_supported(color_modes)
if self.brightness_supported:
@@ -89,7 +97,9 @@ class Light(HomeAccessory):
if self.color_supported:
self.chars.extend([CHAR_HUE, CHAR_SATURATION])
if self.color_temp_supported:
if self.color_temp_supported or COLOR_MODES_WITH_WHITES.intersection(
self.color_modes
):
self.chars.append(CHAR_COLOR_TEMPERATURE)
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
@@ -101,13 +111,22 @@ class Light(HomeAccessory):
# to set to the correct initial value.
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
if self.color_temp_supported:
min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153))
max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500))
if CHAR_COLOR_TEMPERATURE in self.chars:
self.min_mireds = math.floor(
attributes.get(ATTR_MIN_MIREDS, DEFAULT_MIN_MIREDS)
)
self.max_mireds = math.ceil(
attributes.get(ATTR_MAX_MIREDS, DEFAULT_MAX_MIREDS)
)
if not self.color_temp_supported and not self.rgbww_supported:
self.max_mireds = self.min_mireds
self.char_color_temp = serv_light.configure_char(
CHAR_COLOR_TEMPERATURE,
value=min_mireds,
properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds},
value=self.min_mireds,
properties={
PROP_MIN_VALUE: self.min_mireds,
PROP_MAX_VALUE: self.max_mireds,
},
)
if self.color_supported:
@@ -165,33 +184,32 @@ class Light(HomeAccessory):
)
return
# Handle white channels
if CHAR_COLOR_TEMPERATURE in char_values:
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE]
events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}")
temp = char_values[CHAR_COLOR_TEMPERATURE]
events.append(f"color temperature at {temp}")
bright_val = round(
((brightness_pct or self.char_brightness.value) * 255) / 100
)
if self.color_temp_supported:
params[ATTR_COLOR_TEMP] = temp
elif self.rgbww_supported:
params[ATTR_RGBWW_COLOR] = color_temperature_to_rgbww(
temp, bright_val, self.min_mireds, self.max_mireds
)
elif self.rgbw_supported:
params[ATTR_RGBW_COLOR] = (*(0,) * 3, bright_val)
elif self.white_supported:
params[ATTR_WHITE] = bright_val
elif (
CHAR_HUE in char_values
or CHAR_SATURATION in char_values
# If we are adjusting brightness we need to send the full RGBW/RGBWW values
# since HomeKit does not support RGBW/RGBWW
or brightness_pct
and COLOR_MODES_WITH_WHITES.intersection(self.color_modes)
):
elif CHAR_HUE in char_values or CHAR_SATURATION in char_values:
hue_sat = (
char_values.get(CHAR_HUE, self.char_hue.value),
char_values.get(CHAR_SATURATION, self.char_saturation.value),
)
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat)
events.append(f"set color at {hue_sat}")
# HomeKit doesn't support RGBW/RGBWW so we need to remove any white values
if COLOR_MODE_RGBWW in self.color_modes:
val = brightness_pct or self.char_brightness.value
params[ATTR_RGBWW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0, 0)
elif COLOR_MODE_RGBW in self.color_modes:
val = brightness_pct or self.char_brightness.value
params[ATTR_RGBW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0)
else:
params[ATTR_HS_COLOR] = hue_sat
params[ATTR_HS_COLOR] = hue_sat
if (
brightness_pct
@@ -200,6 +218,9 @@ class Light(HomeAccessory):
):
params[ATTR_BRIGHTNESS_PCT] = brightness_pct
_LOGGER.debug(
"Calling light service with params: %s -> %s", char_values, params
)
self.async_call_service(DOMAIN, service, params, ", ".join(events))
@callback
@@ -210,52 +231,59 @@ class Light(HomeAccessory):
attributes = new_state.attributes
color_mode = attributes.get(ATTR_COLOR_MODE)
self.char_on.set_value(int(state == STATE_ON))
color_mode_changed = self._previous_color_mode != color_mode
self._previous_color_mode = color_mode
# Handle Brightness
if self.brightness_supported:
if (
color_mode
and COLOR_MODES_WITH_WHITES.intersection({color_mode})
and (rgb_color := attributes.get(ATTR_RGB_COLOR))
):
# HomeKit doesn't support RGBW/RGBWW so we need to
# give it the color brightness only
brightness = max(rgb_color)
else:
brightness = attributes.get(ATTR_BRIGHTNESS)
if isinstance(brightness, (int, float)):
brightness = round(brightness / 255 * 100, 0)
# The homeassistant component might report its brightness as 0 but is
# not off. But 0 is a special value in homekit. When you turn on a
# homekit accessory it will try to restore the last brightness state
# which will be the last value saved by char_brightness.set_value.
# But if it is set to 0, HomeKit will update the brightness to 100 as
# it thinks 0 is off.
#
# Therefore, if the the brightness is 0 and the device is still on,
# the brightness is mapped to 1 otherwise the update is ignored in
# order to avoid this incorrect behavior.
if brightness == 0 and state == STATE_ON:
brightness = 1
self.char_brightness.set_value(brightness)
if (
self.brightness_supported
and (brightness := attributes.get(ATTR_BRIGHTNESS)) is not None
and isinstance(brightness, (int, float))
):
brightness = round(brightness / 255 * 100, 0)
# The homeassistant component might report its brightness as 0 but is
# not off. But 0 is a special value in homekit. When you turn on a
# homekit accessory it will try to restore the last brightness state
# which will be the last value saved by char_brightness.set_value.
# But if it is set to 0, HomeKit will update the brightness to 100 as
# it thinks 0 is off.
#
# Therefore, if the the brightness is 0 and the device is still on,
# the brightness is mapped to 1 otherwise the update is ignored in
# order to avoid this incorrect behavior.
if brightness == 0 and state == STATE_ON:
brightness = 1
self.char_brightness.set_value(brightness)
if color_mode_changed:
self.char_brightness.notify()
# Handle Color - color must always be set before color temperature
# or the iOS UI will not display it correctly.
if self.color_supported:
if ATTR_COLOR_TEMP in attributes:
if color_temp := attributes.get(ATTR_COLOR_TEMP):
hue, saturation = color_temperature_to_hs(
color_temperature_mired_to_kelvin(
new_state.attributes[ATTR_COLOR_TEMP]
)
color_temperature_mired_to_kelvin(color_temp)
)
elif color_mode == COLOR_MODE_WHITE:
hue, saturation = 0, 0
else:
hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None))
if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)):
self.char_hue.set_value(round(hue, 0))
self.char_saturation.set_value(round(saturation, 0))
if color_mode_changed:
# If the color temp changed, be sure to force the color to update
self.char_hue.notify()
self.char_saturation.notify()
# Handle color temperature
if self.color_temp_supported:
color_temp = attributes.get(ATTR_COLOR_TEMP)
# Handle white channels
if CHAR_COLOR_TEMPERATURE in self.chars:
color_temp = None
if self.color_temp_supported:
color_temp = attributes.get(ATTR_COLOR_TEMP)
elif color_mode == COLOR_MODE_WHITE:
color_temp = self.min_mireds
if isinstance(color_temp, (int, float)):
self.char_color_temp.set_value(round(color_temp, 0))
if color_mode_changed:
self.char_color_temp.notify()

View File

@@ -3,7 +3,7 @@
"name": "Image",
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/image",
"requirements": ["pillow==9.0.0"],
"requirements": ["pillow==9.0.1"],
"dependencies": ["http"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"

View File

@@ -3,7 +3,7 @@
"name": "IntelliFire",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/intellifire",
"requirements": ["intellifire4py==0.5"],
"requirements": ["intellifire4py==0.6"],
"dependencies": [],
"codeowners": ["@jeeftor"],
"iot_class": "local_polling"

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": [
"xknx==0.19.1"
"xknx==0.19.2"
],
"codeowners": [
"@Julius2342",

View File

@@ -35,7 +35,7 @@ from homeassistant.const import (
Platform,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA
from homeassistant.helpers.entity import validate_entity_category
from .const import (
CONF_INVERT,
@@ -320,7 +320,7 @@ class BinarySensorSchema(KNXPlatformSchema):
),
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_RESET_AFTER): cv.positive_float,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
)
@@ -356,7 +356,7 @@ class ButtonSchema(KNXPlatformSchema):
vol.Exclusive(
CONF_TYPE, "length_or_type", msg=length_or_type_msg
): object,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
vol.Any(
@@ -500,7 +500,7 @@ class ClimateSchema(KNXPlatformSchema):
): vol.In(HVAC_MODES),
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
)
@@ -555,7 +555,7 @@ class CoverSchema(KNXPlatformSchema):
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
)
@@ -618,7 +618,7 @@ class FanSchema(KNXPlatformSchema):
vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator,
vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_MAX_STEP): cv.byte,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
)
@@ -722,7 +722,7 @@ class LightSchema(KNXPlatformSchema):
vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
vol.Any(
@@ -802,7 +802,7 @@ class NumberSchema(KNXPlatformSchema):
vol.Optional(CONF_MAX): vol.Coerce(float),
vol.Optional(CONF_MIN): vol.Coerce(float),
vol.Optional(CONF_STEP): cv.positive_float,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
number_limit_sub_validator,
@@ -824,7 +824,7 @@ class SceneSchema(KNXPlatformSchema):
vol.Required(CONF_SCENE_NUMBER): vol.All(
vol.Coerce(int), vol.Range(min=1, max=64)
),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
)
@@ -855,7 +855,7 @@ class SelectSchema(KNXPlatformSchema):
],
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
select_options_sub_validator,
@@ -880,7 +880,7 @@ class SensorSchema(KNXPlatformSchema):
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
vol.Required(CONF_TYPE): sensor_type_validator,
vol.Required(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
)
@@ -901,7 +901,7 @@ class SwitchSchema(KNXPlatformSchema):
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
)
@@ -948,7 +948,7 @@ class WeatherSchema(KNXPlatformSchema):
vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
)

View File

@@ -138,6 +138,11 @@ async def async_setup_entry(
devices = bridge.get_devices()
bridge_device = devices[BRIDGE_DEVICE_ID]
if not config_entry.unique_id:
hass.config_entries.async_update_entry(
config_entry, unique_id=hex(bridge_device["serial"])[2:].zfill(8)
)
buttons = bridge.buttons
_async_register_bridge_device(hass, entry_id, bridge_device)
button_devices = _async_register_button_devices(

View File

@@ -44,7 +44,7 @@ from homeassistant.helpers import (
template,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA
from homeassistant.helpers.entity import validate_entity_category
from homeassistant.util.decorator import Registry
from .const import (
@@ -423,7 +423,7 @@ def _validate_state_class_sensor(value: dict):
vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any(
None, bool, str, int, float
),
vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): validate_entity_category,
vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon,
vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.In(SENSOSR_STATE_CLASSES),
},

View File

@@ -3,7 +3,7 @@
"name": "Motion Blinds",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"requirements": ["motionblinds==0.5.10"],
"requirements": ["motionblinds==0.5.12"],
"dependencies": ["network"],
"codeowners": ["@starkillerOG"],
"iot_class": "local_push"

View File

@@ -577,6 +577,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
websocket_api.async_register_command(hass, websocket_subscribe)
websocket_api.async_register_command(hass, websocket_remove_device)
websocket_api.async_register_command(hass, websocket_mqtt_info)
debug_info.initialize(hass)
if conf is None:
# If we have a config entry, setup is done by that config entry.

View File

@@ -133,6 +133,10 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
self._expired = False
self._state = last_state.state
if self._expiration_trigger:
# We might have set up a trigger already after subscribing from
# super().async_added_to_hass()
self._expiration_trigger()
self._expiration_trigger = async_track_point_in_utc_time(
self.hass, self._value_is_expired, expiration_at
)
@@ -189,7 +193,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
# Reset old trigger
if self._expiration_trigger:
self._expiration_trigger()
self._expiration_trigger = None
# Set new trigger
expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after)

View File

@@ -15,6 +15,11 @@ DATA_MQTT_DEBUG_INFO = "mqtt_debug_info"
STORED_MESSAGES = 10
def initialize(hass: HomeAssistant):
"""Initialize MQTT debug info."""
hass.data[DATA_MQTT_DEBUG_INFO] = {"entities": {}, "triggers": {}}
def log_messages(
hass: HomeAssistant, entity_id: str
) -> Callable[[MessageCallbackType], MessageCallbackType]:
@@ -45,9 +50,7 @@ def log_messages(
def add_subscription(hass, message_callback, subscription):
"""Prepare debug data for subscription."""
if entity_id := getattr(message_callback, "__entity_id", None):
debug_info = hass.data.setdefault(
DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}}
)
debug_info = hass.data[DATA_MQTT_DEBUG_INFO]
entity_info = debug_info["entities"].setdefault(
entity_id, {"subscriptions": {}, "discovery_data": {}}
)
@@ -76,9 +79,7 @@ def remove_subscription(hass, message_callback, subscription):
def add_entity_discovery_data(hass, discovery_data, entity_id):
"""Add discovery data."""
debug_info = hass.data.setdefault(
DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}}
)
debug_info = hass.data[DATA_MQTT_DEBUG_INFO]
entity_info = debug_info["entities"].setdefault(
entity_id, {"subscriptions": {}, "discovery_data": {}}
)
@@ -93,14 +94,13 @@ def update_entity_discovery_data(hass, discovery_payload, entity_id):
def remove_entity_data(hass, entity_id):
"""Remove discovery data."""
hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id)
if entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]:
hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id)
def add_trigger_discovery_data(hass, discovery_hash, discovery_data, device_id):
"""Add discovery data."""
debug_info = hass.data.setdefault(
DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}}
)
debug_info = hass.data[DATA_MQTT_DEBUG_INFO]
debug_info["triggers"][discovery_hash] = {
"device_id": device_id,
"discovery_data": discovery_data,
@@ -126,9 +126,7 @@ async def info_for_device(hass, device_id):
entries = hass.helpers.entity_registry.async_entries_for_device(
entity_registry, device_id, include_disabled_entities=True
)
mqtt_debug_info = hass.data.setdefault(
DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}}
)
mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO]
for entry in entries:
if entry.entity_id not in mqtt_debug_info["entities"]:
continue

View File

@@ -30,11 +30,11 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
)
from homeassistant.helpers.entity import (
ENTITY_CATEGORIES_SCHEMA,
DeviceInfo,
Entity,
EntityCategory,
async_generate_entity_id,
validate_entity_category,
)
from homeassistant.helpers.typing import ConfigType
@@ -191,7 +191,7 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template,
@@ -549,7 +549,6 @@ class MqttDiscoveryUpdate(Entity):
def _cleanup_discovery_on_remove(self) -> None:
"""Stop listening to signal and cleanup discovery data."""
if self._discovery_data and not self._removed_from_hass:
debug_info.remove_entity_data(self.hass, self.entity_id)
clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH])
self._removed_from_hass = True
@@ -677,6 +676,7 @@ class MqttEntity(
await MqttAttributes.async_will_remove_from_hass(self)
await MqttAvailability.async_will_remove_from_hass(self)
await MqttDiscoveryUpdate.async_will_remove_from_hass(self)
debug_info.remove_entity_data(self.hass, self.entity_id)
@staticmethod
@abstractmethod

View File

@@ -180,6 +180,10 @@ class MqttSensor(MqttEntity, SensorEntity, RestoreEntity):
self._expired = False
self._state = last_state.state
if self._expiration_trigger:
# We might have set up a trigger already after subscribing from
# super().async_added_to_hass()
self._expiration_trigger()
self._expiration_trigger = async_track_point_in_utc_time(
self.hass, self._value_is_expired, expiration_at
)
@@ -227,7 +231,6 @@ class MqttSensor(MqttEntity, SensorEntity, RestoreEntity):
# Reset old trigger
if self._expiration_trigger:
self._expiration_trigger()
self._expiration_trigger = None
# Set new trigger
expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after)

View File

@@ -12,7 +12,7 @@ from google_nest_sdm.exceptions import ApiException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DATA_SUBSCRIBER, DOMAIN
from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN
REDACT_DEVICE_TRAITS = {InfoTrait.NAME}
@@ -21,6 +21,9 @@ async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict:
"""Return diagnostics for a config entry."""
if DATA_SDM not in config_entry.data:
return {}
if DATA_SUBSCRIBER not in hass.data[DOMAIN]:
return {"error": "No subscriber configured"}

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["ffmpeg", "http", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/nest",
"requirements": ["python-nest==4.1.0", "google-nest-sdm==1.6.0"],
"requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.1"],
"codeowners": ["@allenporter"],
"quality_scale": "platinum",
"dhcp": [

View File

@@ -26,7 +26,7 @@ CONST_LIST_E_TO_H: list[str] = ["E", "F", "G", "H"]
CONST_LIST_I_TO_L: list[str] = ["I", "J", "K", "L"]
CONST_LIST_M_TO_Q: list[str] = ["M", "N", "O", "Ö", "P", "Q"]
CONST_LIST_R_TO_U: list[str] = ["R", "S", "T", "U", "Ü"]
CONST_LIST_V_TO_Z: list[str] = ["V", "W", "X", "Y"]
CONST_LIST_V_TO_Z: list[str] = ["V", "W", "X", "Y", "Z"]
CONST_REGION_A_TO_D: Final = "_a_to_d"
CONST_REGION_E_TO_H: Final = "_e_to_h"

View File

@@ -121,14 +121,22 @@ async def async_setup_entry(
if coordinator.data:
if coordinator.data.electricity:
for description in SENSOR_TYPES_ELECTRICITY:
if description.key == KEY_LAST_ELECTRICITY_COST:
if (
description.key == KEY_LAST_ELECTRICITY_COST
and coordinator.data.electricity[-1] is not None
and coordinator.data.electricity[-1].cost is not None
):
description.native_unit_of_measurement = (
coordinator.data.electricity[-1].cost.currency_unit
)
entities.append(OVOEnergySensor(coordinator, description, client))
if coordinator.data.gas:
for description in SENSOR_TYPES_GAS:
if description.key == KEY_LAST_GAS_COST:
if (
description.key == KEY_LAST_GAS_COST
and coordinator.data.gas[-1] is not None
and coordinator.data.gas[-1].cost is not None
):
description.native_unit_of_measurement = coordinator.data.gas[
-1
].cost.currency_unit

View File

@@ -82,7 +82,7 @@ async def async_setup_entry(
class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
"""Representation of a Philips TV exposing the JointSpace API."""
_coordinator: PhilipsTVDataUpdateCoordinator
coordinator: PhilipsTVDataUpdateCoordinator
_attr_device_class = MediaPlayerDeviceClass.TV
def __init__(
@@ -91,7 +91,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
) -> None:
"""Initialize the Philips TV."""
self._tv = coordinator.api
self._coordinator = coordinator
self._sources = {}
self._channels = {}
self._supports = SUPPORT_PHILIPS_JS
@@ -125,7 +124,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
def supported_features(self):
"""Flag media player features that are supported."""
supports = self._supports
if self._coordinator.turn_on or (
if self.coordinator.turn_on or (
self._tv.on and self._tv.powerstate is not None
):
supports |= SUPPORT_TURN_ON
@@ -170,7 +169,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
await self._tv.setPowerState("On")
self._state = STATE_ON
else:
await self._coordinator.turn_on.async_run(self.hass, self._context)
await self.coordinator.turn_on.async_run(self.hass, self._context)
await self._async_update_soon()
async def async_turn_off(self):

View File

@@ -30,7 +30,7 @@ async def async_setup_entry(
class PhilipsTVRemote(CoordinatorEntity, RemoteEntity):
"""Device that sends commands."""
_coordinator: PhilipsTVDataUpdateCoordinator
coordinator: PhilipsTVDataUpdateCoordinator
def __init__(
self,
@@ -63,7 +63,7 @@ class PhilipsTVRemote(CoordinatorEntity, RemoteEntity):
if self._tv.on and self._tv.powerstate:
await self._tv.setPowerState("On")
else:
await self._coordinator.turn_on.async_run(self.hass, self._context)
await self.coordinator.turn_on.async_run(self.hass, self._context)
self.async_write_ha_state()
async def async_turn_off(self, **kwargs):

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/plex",
"requirements": [
"plexapi==4.9.1",
"plexapi==4.9.2",
"plexauth==0.0.6",
"plexwebsocket==0.0.13"
],

View File

@@ -2,6 +2,6 @@
"domain": "proxy",
"name": "Camera Proxy",
"documentation": "https://www.home-assistant.io/integrations/proxy",
"requirements": ["pillow==9.0.0"],
"requirements": ["pillow==9.0.1"],
"codeowners": []
}

View File

@@ -23,7 +23,7 @@ async def validate_input(hass: HomeAssistant, *, api_key: str, system_id: int) -
api_key=api_key,
system_id=system_id,
)
await pvoutput.status()
await pvoutput.system()
class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN):

View File

@@ -1,14 +1,14 @@
"""DataUpdateCoordinator for the PVOutput integration."""
from __future__ import annotations
from pvo import PVOutput, PVOutputAuthenticationError, Status
from pvo import PVOutput, PVOutputAuthenticationError, PVOutputNoDataError, Status
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SYSTEM_ID, DOMAIN, LOGGER, SCAN_INTERVAL
@@ -33,5 +33,7 @@ class PVOutputDataUpdateCoordinator(DataUpdateCoordinator[Status]):
"""Fetch system status from PVOutput."""
try:
return await self.pvoutput.status()
except PVOutputNoDataError as err:
raise UpdateFailed("PVOutput has no data available") from err
except PVOutputAuthenticationError as err:
raise ConfigEntryAuthFailed from err

View File

@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/pvoutput",
"config_flow": true,
"codeowners": ["@fabaff", "@frenck"],
"requirements": ["pvo==0.2.1"],
"requirements": ["pvo==0.2.2"],
"iot_class": "cloud_polling",
"quality_scale": "platinum"
}

View File

@@ -2,7 +2,7 @@
"domain": "qrcode",
"name": "QR Code",
"documentation": "https://www.home-assistant.io/integrations/qrcode",
"requirements": ["pillow==9.0.0", "pyzbar==0.1.7"],
"requirements": ["pillow==9.0.1", "pyzbar==0.1.7"],
"codeowners": [],
"iot_class": "calculated"
}

View File

@@ -40,7 +40,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md"
)
hass.data[DOMAIN][I2C_HATS_MANAGER] = I2CHatsManager()
hass.data[DOMAIN] = {I2C_HATS_MANAGER: I2CHatsManager()}
def start_i2c_hats_keep_alive(event):
"""Start I2C-HATs keep alive."""

View File

@@ -101,15 +101,15 @@ def _create_index(instance, table_name, index_name):
"be patient!",
index_name,
)
try:
with session_scope(session=instance.get_session()) as session:
with session_scope(session=instance.get_session()) as session:
try:
connection = session.connection()
index.create(connection)
except (InternalError, OperationalError, ProgrammingError) as err:
raise_if_exception_missing_str(err, ["already exists", "duplicate"])
_LOGGER.warning(
"Index %s already exists on %s, continuing", index_name, table_name
)
except (InternalError, OperationalError, ProgrammingError) as err:
raise_if_exception_missing_str(err, ["already exists", "duplicate"])
_LOGGER.warning(
"Index %s already exists on %s, continuing", index_name, table_name
)
_LOGGER.debug("Finished creating %s", index_name)
@@ -129,19 +129,19 @@ def _drop_index(instance, table_name, index_name):
success = False
# Engines like DB2/Oracle
try:
with session_scope(session=instance.get_session()) as session:
with session_scope(session=instance.get_session()) as session:
try:
connection = session.connection()
connection.execute(text(f"DROP INDEX {index_name}"))
except SQLAlchemyError:
pass
else:
success = True
except SQLAlchemyError:
pass
else:
success = True
# Engines like SQLite, SQL Server
if not success:
try:
with session_scope(session=instance.get_session()) as session:
with session_scope(session=instance.get_session()) as session:
try:
connection = session.connection()
connection.execute(
text(
@@ -150,15 +150,15 @@ def _drop_index(instance, table_name, index_name):
)
)
)
except SQLAlchemyError:
pass
else:
success = True
except SQLAlchemyError:
pass
else:
success = True
if not success:
# Engines like MySQL, MS Access
try:
with session_scope(session=instance.get_session()) as session:
with session_scope(session=instance.get_session()) as session:
try:
connection = session.connection()
connection.execute(
text(
@@ -167,10 +167,10 @@ def _drop_index(instance, table_name, index_name):
)
)
)
except SQLAlchemyError:
pass
else:
success = True
except SQLAlchemyError:
pass
else:
success = True
if success:
_LOGGER.debug(
@@ -203,8 +203,8 @@ def _add_columns(instance, table_name, columns_def):
columns_def = [f"ADD {col_def}" for col_def in columns_def]
try:
with session_scope(session=instance.get_session()) as session:
with session_scope(session=instance.get_session()) as session:
try:
connection = session.connection()
connection.execute(
text(
@@ -214,14 +214,14 @@ def _add_columns(instance, table_name, columns_def):
)
)
return
except (InternalError, OperationalError, ProgrammingError):
# Some engines support adding all columns at once,
# this error is when they don't
_LOGGER.info("Unable to use quick column add. Adding 1 by 1")
except (InternalError, OperationalError, ProgrammingError):
# Some engines support adding all columns at once,
# this error is when they don't
_LOGGER.info("Unable to use quick column add. Adding 1 by 1")
for column_def in columns_def:
try:
with session_scope(session=instance.get_session()) as session:
with session_scope(session=instance.get_session()) as session:
try:
connection = session.connection()
connection.execute(
text(
@@ -230,13 +230,13 @@ def _add_columns(instance, table_name, columns_def):
)
)
)
except (InternalError, OperationalError, ProgrammingError) as err:
raise_if_exception_missing_str(err, ["already exists", "duplicate"])
_LOGGER.warning(
"Column %s already exists on %s, continuing",
column_def.split(" ")[1],
table_name,
)
except (InternalError, OperationalError, ProgrammingError) as err:
raise_if_exception_missing_str(err, ["already exists", "duplicate"])
_LOGGER.warning(
"Column %s already exists on %s, continuing",
column_def.split(" ")[1],
table_name,
)
def _modify_columns(instance, engine, table_name, columns_def):
@@ -271,8 +271,8 @@ def _modify_columns(instance, engine, table_name, columns_def):
else:
columns_def = [f"MODIFY {col_def}" for col_def in columns_def]
try:
with session_scope(session=instance.get_session()) as session:
with session_scope(session=instance.get_session()) as session:
try:
connection = session.connection()
connection.execute(
text(
@@ -282,12 +282,12 @@ def _modify_columns(instance, engine, table_name, columns_def):
)
)
return
except (InternalError, OperationalError):
_LOGGER.info("Unable to use quick column modify. Modifying 1 by 1")
except (InternalError, OperationalError):
_LOGGER.info("Unable to use quick column modify. Modifying 1 by 1")
for column_def in columns_def:
try:
with session_scope(session=instance.get_session()) as session:
with session_scope(session=instance.get_session()) as session:
try:
connection = session.connection()
connection.execute(
text(
@@ -296,10 +296,10 @@ def _modify_columns(instance, engine, table_name, columns_def):
)
)
)
except (InternalError, OperationalError):
_LOGGER.exception(
"Could not modify column %s in table %s", column_def, table_name
)
except (InternalError, OperationalError):
_LOGGER.exception(
"Could not modify column %s in table %s", column_def, table_name
)
def _update_states_table_with_foreign_key_options(instance, engine):
@@ -330,17 +330,17 @@ def _update_states_table_with_foreign_key_options(instance, engine):
)
for alter in alters:
try:
with session_scope(session=instance.get_session()) as session:
with session_scope(session=instance.get_session()) as session:
try:
connection = session.connection()
connection.execute(DropConstraint(alter["old_fk"]))
for fkc in states_key_constraints:
if fkc.column_keys == alter["columns"]:
connection.execute(AddConstraint(fkc))
except (InternalError, OperationalError):
_LOGGER.exception(
"Could not update foreign options in %s table", TABLE_STATES
)
except (InternalError, OperationalError):
_LOGGER.exception(
"Could not update foreign options in %s table", TABLE_STATES
)
def _drop_foreign_key_constraints(instance, engine, table, columns):
@@ -361,16 +361,16 @@ def _drop_foreign_key_constraints(instance, engine, table, columns):
)
for drop in drops:
try:
with session_scope(session=instance.get_session()) as session:
with session_scope(session=instance.get_session()) as session:
try:
connection = session.connection()
connection.execute(DropConstraint(drop))
except (InternalError, OperationalError):
_LOGGER.exception(
"Could not drop foreign constraints in %s table on %s",
TABLE_STATES,
columns,
)
except (InternalError, OperationalError):
_LOGGER.exception(
"Could not drop foreign constraints in %s table on %s",
TABLE_STATES,
columns,
)
def _apply_update(instance, new_version, old_version): # noqa: C901

View File

@@ -290,7 +290,7 @@ def _find_duplicates(
)
.filter(subquery.c.is_duplicate == 1)
.order_by(table.metadata_id, table.start, table.id.desc())
.limit(MAX_ROWS_TO_PURGE)
.limit(1000 * MAX_ROWS_TO_PURGE)
)
duplicates = execute(query)
original_as_dict = {}
@@ -343,12 +343,13 @@ def _delete_duplicates_from_table(
if not duplicate_ids:
break
all_non_identical_duplicates.extend(non_identical_duplicates)
deleted_rows = (
session.query(table)
.filter(table.id.in_(duplicate_ids))
.delete(synchronize_session=False)
)
total_deleted_rows += deleted_rows
for i in range(0, len(duplicate_ids), MAX_ROWS_TO_PURGE):
deleted_rows = (
session.query(table)
.filter(table.id.in_(duplicate_ids[i : i + MAX_ROWS_TO_PURGE]))
.delete(synchronize_session=False)
)
total_deleted_rows += deleted_rows
return (total_deleted_rows, all_non_identical_duplicates)

View File

@@ -20,7 +20,6 @@ from homeassistant.const import (
CONF_TYPE,
CONF_USERNAME,
ENERGY_KILO_WATT_HOUR,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
MASS_KILOGRAMS,
POWER_WATT,
@@ -33,6 +32,7 @@ from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.start import async_at_start
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
@@ -131,17 +131,19 @@ async def async_setup_platform(
return values
@callback
def start_update_interval(event):
"""Start the update interval scheduling."""
nonlocal remove_interval_update
remove_interval_update = async_track_time_interval_backoff(hass, async_saj)
@callback
def stop_update_interval(event):
"""Properly cancel the scheduled update."""
remove_interval_update() # pylint: disable=not-callable
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_update_interval)
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval)
async_at_start(hass, start_update_interval)
@callback

View File

@@ -2,7 +2,7 @@
"domain": "seven_segments",
"name": "Seven Segments OCR",
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
"requirements": ["pillow==9.0.0"],
"requirements": ["pillow==9.0.1"],
"codeowners": ["@fabaff"],
"iot_class": "local_polling"
}

View File

@@ -3,7 +3,7 @@
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==1.0.8"],
"requirements": ["aioshelly==1.0.9"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -2,7 +2,7 @@
"domain": "sighthound",
"name": "Sighthound",
"documentation": "https://www.home-assistant.io/integrations/sighthound",
"requirements": ["pillow==9.0.0", "simplehound==0.3"],
"requirements": ["pillow==9.0.1", "simplehound==0.3"],
"codeowners": ["@robmarkcole"],
"iot_class": "cloud_polling"
}

View File

@@ -482,6 +482,7 @@ class SimpliSafe:
self._websocket_reconnect_task: asyncio.Task | None = None
self.entry = entry
self.initial_event_to_use: dict[int, dict[str, Any]] = {}
self.subscription_data: dict[int, Any] = api.subscription_data
self.systems: dict[int, SystemType] = {}
# This will get filled in by async_init:

View File

@@ -11,14 +11,28 @@ from homeassistant.core import HomeAssistant
from . import SimpliSafe
from .const import DOMAIN
CONF_CREDIT_CARD = "creditCard"
CONF_EXPIRES = "expires"
CONF_LOCATION = "location"
CONF_LOCATION_NAME = "locationName"
CONF_PAYMENT_PROFILE_ID = "paymentProfileId"
CONF_SERIAL = "serial"
CONF_SID = "sid"
CONF_SYSTEM_ID = "system_id"
CONF_UID = "uid"
CONF_WIFI_SSID = "wifi_ssid"
TO_REDACT = {
CONF_ADDRESS,
CONF_CREDIT_CARD,
CONF_EXPIRES,
CONF_LOCATION,
CONF_LOCATION_NAME,
CONF_PAYMENT_PROFILE_ID,
CONF_SERIAL,
CONF_SID,
CONF_SYSTEM_ID,
CONF_UID,
CONF_WIFI_SSID,
}
@@ -34,6 +48,7 @@ async def async_get_config_entry_diagnostics(
"entry": {
"options": dict(entry.options),
},
"subscription_data": simplisafe.subscription_data,
"systems": [system.as_dict() for system in simplisafe.systems.values()],
},
TO_REDACT,

View File

@@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==2022.01.0"],
"requirements": ["simplisafe-python==2022.02.1"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"dhcp": [

View File

@@ -44,6 +44,7 @@ SONOS_ALBUM_ARTIST = "album_artists"
SONOS_TRACKS = "tracks"
SONOS_COMPOSER = "composers"
SONOS_RADIO = "radio"
SONOS_OTHER_ITEM = "other items"
SONOS_STATE_PLAYING = "PLAYING"
SONOS_STATE_TRANSITIONING = "TRANSITIONING"
@@ -76,6 +77,7 @@ SONOS_TO_MEDIA_CLASSES = {
"object.container.person.musicArtist": MEDIA_CLASS_ARTIST,
"object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST,
"object.container.playlistContainer": MEDIA_CLASS_PLAYLIST,
"object.item": MEDIA_CLASS_TRACK,
"object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK,
"object.item.audioItem.audioBroadcast": MEDIA_CLASS_GENRE,
}
@@ -121,6 +123,7 @@ SONOS_TYPES_MAPPING = {
"object.container.person.musicArtist": SONOS_ALBUM_ARTIST,
"object.container.playlistContainer.sameArtist": SONOS_ARTIST,
"object.container.playlistContainer": SONOS_PLAYLISTS,
"object.item": SONOS_OTHER_ITEM,
"object.item.audioItem.musicTrack": SONOS_TRACKS,
"object.item.audioItem.audioBroadcast": SONOS_RADIO,
}

View File

@@ -3,7 +3,7 @@
"name": "Sonos",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["soco==0.26.0"],
"requirements": ["soco==0.26.2"],
"dependencies": ["ssdp"],
"after_dependencies": ["plex", "spotify", "zeroconf", "media_source"],
"zeroconf": ["_sonos._tcp.local."],

View File

@@ -162,8 +162,17 @@ def build_item_response(media_library, payload, get_thumbnail_url=None):
payload["idstring"].split("/")[2:]
)
try:
search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]]
except KeyError:
_LOGGER.debug(
"Unknown media type received when building item response: %s",
payload["search_type"],
)
return
media = media_library.browse_by_idstring(
MEDIA_TYPES_TO_SONOS[payload["search_type"]],
search_type,
payload["idstring"],
full_album_art_uri=True,
max_items=0,
@@ -371,11 +380,16 @@ def favorites_payload(favorites):
group_types = {fav.reference.item_class for fav in favorites}
for group_type in sorted(group_types):
media_content_type = SONOS_TYPES_MAPPING[group_type]
try:
media_content_type = SONOS_TYPES_MAPPING[group_type]
media_class = SONOS_TO_MEDIA_CLASSES[group_type]
except KeyError:
_LOGGER.debug("Unknown media type or class received %s", group_type)
continue
children.append(
BrowseMedia(
title=media_content_type.title(),
media_class=SONOS_TO_MEDIA_CLASSES[group_type],
media_class=media_class,
media_content_id=group_type,
media_content_type="favorites_folder",
can_play=False,

View File

@@ -558,7 +558,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
plex_plugin.play_now(media)
return
share_link = self.speaker.share_link
share_link = self.coordinator.share_link
if share_link.is_share_link(media_id):
if kwargs.get(ATTR_MEDIA_ENQUEUE):
share_link.add_share_link_to_queue(media_id)

View File

@@ -399,13 +399,20 @@ class SonosSpeaker:
return_exceptions=True,
)
for result in results:
if isinstance(result, Exception):
_LOGGER.debug(
"Unsubscribe failed for %s: %s",
self.zone_name,
result,
exc_info=result,
)
if isinstance(result, asyncio.exceptions.TimeoutError):
message = "Request timed out"
exc_info = None
elif isinstance(result, Exception):
message = result
exc_info = result if not str(result) else None
else:
continue
_LOGGER.debug(
"Unsubscribe failed for %s: %s",
self.zone_name,
message,
exc_info=exc_info,
)
self._subscriptions = []
@callback
@@ -422,19 +429,18 @@ class SonosSpeaker:
if not self.available:
return
if getattr(exception, "status", None) == 412:
_LOGGER.warning(
"Subscriptions for %s failed, speaker may have lost power",
self.zone_name,
)
if isinstance(exception, asyncio.exceptions.TimeoutError):
message = "Request timed out"
exc_info = None
else:
exc_info = exception if _LOGGER.isEnabledFor(logging.DEBUG) else None
_LOGGER.error(
"Subscription renewals for %s failed: %s",
self.zone_name,
exception,
exc_info=exc_info,
)
message = exception
exc_info = exception if not str(exception) else None
_LOGGER.warning(
"Subscription renewals for %s failed, marking unavailable: %s",
self.zone_name,
message,
exc_info=exc_info,
)
await self.async_offline()
@callback

View File

@@ -1,6 +1,11 @@
"""The spotify integration."""
from __future__ import annotations
from datetime import timedelta
from typing import Any
import aiohttp
import requests
from spotipy import Spotify, SpotifyException
import voluptuous as vol
@@ -20,13 +25,16 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
async_get_config_entry_implementation,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from . import config_flow
from .const import (
DATA_SPOTIFY_CLIENT,
DATA_SPOTIFY_DEVICES,
DATA_SPOTIFY_ME,
DATA_SPOTIFY_SESSION,
DOMAIN,
LOGGER,
MEDIA_PLAYER_PREFIX,
SPOTIFY_SCOPES,
)
@@ -112,9 +120,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except SpotifyException as err:
raise ConfigEntryNotReady from err
async def _update_devices() -> list[dict[str, Any]]:
try:
devices: dict[str, Any] | None = await hass.async_add_executor_job(
spotify.devices
)
except (requests.RequestException, SpotifyException) as err:
raise UpdateFailed from err
if devices is None:
return []
return devices.get("devices", [])
device_coordinator: DataUpdateCoordinator[
list[dict[str, Any]]
] = DataUpdateCoordinator(
hass,
LOGGER,
name=f"{entry.title} Devices",
update_interval=timedelta(minutes=5),
update_method=_update_devices,
)
await device_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
DATA_SPOTIFY_CLIENT: spotify,
DATA_SPOTIFY_DEVICES: device_coordinator,
DATA_SPOTIFY_ME: current_user,
DATA_SPOTIFY_SESSION: session,
}

View File

@@ -1,8 +1,13 @@
"""Define constants for the Spotify integration."""
import logging
DOMAIN = "spotify"
LOGGER = logging.getLogger(__package__)
DATA_SPOTIFY_CLIENT = "spotify_client"
DATA_SPOTIFY_DEVICES = "spotify_devices"
DATA_SPOTIFY_ME = "spotify_me"
DATA_SPOTIFY_SESSION = "spotify_session"

View File

@@ -52,7 +52,7 @@ from homeassistant.const import (
STATE_PAUSED,
STATE_PLAYING,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.device_registry import DeviceEntryType
@@ -62,6 +62,7 @@ from homeassistant.util.dt import utc_from_timestamp
from .const import (
DATA_SPOTIFY_CLIENT,
DATA_SPOTIFY_DEVICES,
DATA_SPOTIFY_ME,
DATA_SPOTIFY_SESSION,
DOMAIN,
@@ -269,7 +270,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
)
self._currently_playing: dict | None = {}
self._devices: list[dict] | None = []
self._playlist: dict | None = None
self._attr_name = self._name
@@ -290,6 +290,11 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
"""Return spotify API."""
return self._spotify_data[DATA_SPOTIFY_CLIENT]
@property
def _devices(self) -> list:
"""Return spotify devices."""
return self._spotify_data[DATA_SPOTIFY_DEVICES].data
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this entity."""
@@ -517,13 +522,13 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
current = self._spotify.current_playback()
self._currently_playing = current or {}
self._playlist = None
context = self._currently_playing.get("context")
if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST:
self._playlist = self._spotify.playlist(current["context"]["uri"])
devices = self._spotify.devices() or {}
self._devices = devices.get("devices", [])
if context is not None and (
self._playlist is None or self._playlist["uri"] != context["uri"]
):
self._playlist = None
if context["type"] == MEDIA_TYPE_PLAYLIST:
self._playlist = self._spotify.playlist(current["context"]["uri"])
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
@@ -543,6 +548,22 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
media_content_id,
)
@callback
def _handle_devices_update(self) -> None:
"""Handle updated data from the coordinator."""
if not self.enabled:
return
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
self._spotify_data[DATA_SPOTIFY_DEVICES].async_add_listener(
self._handle_devices_update
)
)
async def async_browse_media_internal(
hass,

View File

@@ -338,7 +338,6 @@ class Stream:
)
except StreamWorkerError as err:
self._logger.error("Error from stream worker: %s", str(err))
self._available = False
stream_state.discontinuity()
if not self.keepalive or self._thread_quit.is_set():

View File

@@ -16,6 +16,7 @@ from synology_dsm.api.storage.storage import SynoStorage
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
from synology_dsm.exceptions import (
SynologyDSMAPIErrorException,
SynologyDSMException,
SynologyDSMLoginFailedException,
SynologyDSMRequestException,
)
@@ -32,7 +33,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from .const import CONF_DEVICE_TOKEN
from .const import CONF_DEVICE_TOKEN, DOMAIN, SYSTEM_LOADED
LOGGER = logging.getLogger(__name__)
@@ -217,6 +218,11 @@ class SynoApi:
)
self.surveillance_station = self.dsm.surveillance_station
def _set_system_loaded(self, state: bool = False) -> None:
"""Set system loaded flag."""
dsm_device = self._hass.data[DOMAIN].get(self.information.serial)
dsm_device[SYSTEM_LOADED] = state
async def _syno_api_executer(self, api_call: Callable) -> None:
"""Synology api call wrapper."""
try:
@@ -230,14 +236,20 @@ class SynoApi:
async def async_reboot(self) -> None:
"""Reboot NAS."""
await self._syno_api_executer(self.system.reboot)
self._set_system_loaded()
async def async_shutdown(self) -> None:
"""Shutdown NAS."""
await self._syno_api_executer(self.system.shutdown)
self._set_system_loaded()
async def async_unload(self) -> None:
"""Stop interacting with the NAS and prepare for removal from hass."""
await self._syno_api_executer(self.dsm.logout)
try:
await self._syno_api_executer(self.dsm.logout)
except SynologyDSMException:
# ignore API errors during logout
pass
async def async_update(self, now: timedelta | None = None) -> None:
"""Update function for updating API information."""

View File

@@ -267,7 +267,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
and existing_entry.data[CONF_HOST] != parsed_url.hostname
and not fqdn_with_ssl_verification
):
_LOGGER.debug(
_LOGGER.info(
"Update host from '%s' to '%s' for NAS '%s' via SSDP discovery",
existing_entry.data[CONF_HOST],
parsed_url.hostname,

View File

@@ -2,7 +2,7 @@
"domain": "synology_dsm",
"name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"requirements": ["py-synologydsm-api==1.0.5"],
"requirements": ["py-synologydsm-api==1.0.6"],
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
"config_flow": true,
"ssdp": [

View File

@@ -15,7 +15,6 @@ from .const import (
SERVICE_SHUTDOWN,
SERVICES,
SYNO_API,
SYSTEM_LOADED,
)
LOGGER = logging.getLogger(__name__)
@@ -57,7 +56,6 @@ async def async_setup_services(hass: HomeAssistant) -> None:
)
dsm_api: SynoApi = dsm_device[SYNO_API]
try:
dsm_device[SYSTEM_LOADED] = False
await getattr(dsm_api, f"async_{call.service}")()
except SynologyDSMException as ex:
LOGGER.error(

View File

@@ -7,7 +7,7 @@
"tf-models-official==2.5.0",
"pycocotools==2.0.1",
"numpy==1.21.4",
"pillow==9.0.0"
"pillow==9.0.1"
],
"codeowners": [],
"iot_class": "local_polling"

View File

@@ -161,6 +161,21 @@ class TodSensor(BinarySensorEntity):
self._time_before = before_event_date
# We are calculating the _time_after value assuming that it will happen today
# But that is not always true, e.g. after 23:00, before 12:00 and now is 10:00
# If _time_before and _time_after are ahead of nowutc:
# _time_before is set to 12:00 next day
# _time_after is set to 23:00 today
# nowutc is set to 10:00 today
if (
not is_sun_event(self._after)
and self._time_after > nowutc
and self._time_before > nowutc + timedelta(days=1)
):
# remove one day from _time_before and _time_after
self._time_after -= timedelta(days=1)
self._time_before -= timedelta(days=1)
# Add offset to utc boundaries according to the configuration
self._time_after += self._after_offset
self._time_before += self._before_offset

View File

@@ -12,7 +12,7 @@ from async_upnp_client.exceptions import UpnpError
from async_upnp_client.profiles.igd import IgdDevice
from homeassistant.components import ssdp
from homeassistant.components.ssdp import SsdpChange
from homeassistant.components.ssdp import SsdpChange, SsdpServiceInfo
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -71,19 +71,22 @@ class Device:
return device
async def async_ssdp_callback(
self, headers: Mapping[str, Any], change: SsdpChange
self, service_info: SsdpServiceInfo, change: SsdpChange
) -> None:
"""SSDP callback, update if needed."""
_LOGGER.debug("SSDP Callback, change: %s, headers: %s", change, headers)
if ssdp.ATTR_SSDP_LOCATION not in headers:
_LOGGER.debug(
"SSDP Callback, change: %s, headers: %s", change, service_info.ssdp_headers
)
if service_info.ssdp_location is None:
return
location = headers[ssdp.ATTR_SSDP_LOCATION]
device = self._igd_device.device
if location == device.device_url:
if service_info.ssdp_location == device.device_url:
return
new_upnp_device = await async_create_upnp_device(self.hass, location)
new_upnp_device = await async_create_upnp_device(
self.hass, service_info.ssdp_location
)
device.reinit(new_upnp_device)
@property

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
from typing import Any
from pyhaversion.consts import HaVersionChannel, HaVersionSource
import voluptuous as vol
from homeassistant import config_entries
@@ -75,8 +74,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._entry_data.update(user_input)
if not self.show_advanced_options or user_input[CONF_SOURCE] in (
HaVersionSource.LOCAL,
HaVersionSource.HAIO,
"local",
"haio",
):
return self.async_create_entry(
title=self._config_entry_name,
@@ -92,8 +91,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle the version_source step."""
if user_input is None:
if self._entry_data[CONF_SOURCE] in (
HaVersionSource.SUPERVISOR,
HaVersionSource.CONTAINER,
"supervisor",
"container",
):
data_schema = vol.Schema(
{
@@ -102,7 +101,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
): vol.In(VALID_CHANNELS),
}
)
if self._entry_data[CONF_SOURCE] == HaVersionSource.SUPERVISOR:
if self._entry_data[CONF_SOURCE] == "supervisor":
data_schema = data_schema.extend(
{
vol.Required(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In(
@@ -151,7 +150,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@property
def _config_entry_name(self) -> str:
"""Return the name of the config entry."""
if self._entry_data[CONF_SOURCE] == HaVersionSource.LOCAL:
if self._entry_data[CONF_SOURCE] == "local":
return DEFAULT_NAME_CURRENT
name = self._entry_data[CONF_VERSION_SOURCE]
@@ -166,21 +165,21 @@ def _convert_imported_configuration(config: dict[str, Any]) -> Any:
"""Convert a key from the imported configuration."""
data = DEFAULT_CONFIGURATION.copy()
if config.get(CONF_BETA):
data[CONF_CHANNEL] = HaVersionChannel.BETA
data[CONF_CHANNEL] = "beta"
if (source := config.get(CONF_SOURCE)) and source != DEFAULT_SOURCE:
if source == SOURCE_HASSIO:
data[CONF_SOURCE] = HaVersionSource.SUPERVISOR
data[CONF_SOURCE] = "supervisor"
data[CONF_VERSION_SOURCE] = VERSION_SOURCE_VERSIONS
elif source == SOURCE_DOKCER:
data[CONF_SOURCE] = HaVersionSource.CONTAINER
data[CONF_SOURCE] = "container"
data[CONF_VERSION_SOURCE] = VERSION_SOURCE_DOCKER_HUB
else:
data[CONF_SOURCE] = source
data[CONF_VERSION_SOURCE] = VERSION_SOURCE_MAP_INVERTED[source]
if (image := config.get(CONF_IMAGE)) and image != DEFAULT_IMAGE:
if data[CONF_SOURCE] == HaVersionSource.CONTAINER:
if data[CONF_SOURCE] == "container":
data[CONF_IMAGE] = f"{config[CONF_IMAGE]}{POSTFIX_CONTAINER_NAME}"
else:
data[CONF_IMAGE] = config[CONF_IMAGE]
@@ -188,7 +187,7 @@ def _convert_imported_configuration(config: dict[str, Any]) -> Any:
if (name := config.get(CONF_NAME)) and name != DEFAULT_NAME:
data[CONF_NAME] = config[CONF_NAME]
else:
if data[CONF_SOURCE] == HaVersionSource.LOCAL:
if data[CONF_SOURCE] == "local":
data[CONF_NAME] = DEFAULT_NAME_CURRENT
else:
data[CONF_NAME] = DEFAULT_NAME_LATEST

View File

@@ -41,12 +41,12 @@ VERSION_SOURCE_VERSIONS: Final = "Home Assistant Versions"
DEFAULT_BETA: Final = False
DEFAULT_BOARD: Final = "OVA"
DEFAULT_CHANNEL: Final[HaVersionChannel] = HaVersionChannel.STABLE
DEFAULT_CHANNEL: Final = "stable"
DEFAULT_IMAGE: Final = "default"
DEFAULT_NAME_CURRENT: Final = "Current Version"
DEFAULT_NAME_LATEST: Final = "Latest Version"
DEFAULT_NAME: Final = ""
DEFAULT_SOURCE: Final[HaVersionSource] = HaVersionSource.LOCAL
DEFAULT_SOURCE: Final = "local"
DEFAULT_CONFIGURATION: Final[dict[str, Any]] = {
CONF_NAME: DEFAULT_NAME,
CONF_CHANNEL: DEFAULT_CHANNEL,
@@ -81,22 +81,22 @@ BOARD_MAP: Final[dict[str, str]] = {
VALID_BOARDS: Final[list[str]] = list(BOARD_MAP)
VERSION_SOURCE_MAP: Final[dict[str, HaVersionSource]] = {
VERSION_SOURCE_LOCAL: HaVersionSource.LOCAL,
VERSION_SOURCE_VERSIONS: HaVersionSource.SUPERVISOR,
VERSION_SOURCE_HAIO: HaVersionSource.HAIO,
VERSION_SOURCE_DOCKER_HUB: HaVersionSource.CONTAINER,
VERSION_SOURCE_PYPI: HaVersionSource.PYPI,
VERSION_SOURCE_MAP: Final[dict[str, str]] = {
VERSION_SOURCE_LOCAL: "local",
VERSION_SOURCE_VERSIONS: "supervisor",
VERSION_SOURCE_HAIO: "haio",
VERSION_SOURCE_DOCKER_HUB: "container",
VERSION_SOURCE_PYPI: "pypi",
}
VERSION_SOURCE_MAP_INVERTED: Final[dict[HaVersionSource, str]] = {
VERSION_SOURCE_MAP_INVERTED: Final[dict[str, str]] = {
value: key for key, value in VERSION_SOURCE_MAP.items()
}
VALID_SOURCES: Final[list[str]] = HA_VERSION_SOURCES + [
SOURCE_HASSIO, # Kept to not break existing configurations
SOURCE_DOKCER, # Kept to not break existing configurations
"hassio", # Kept to not break existing configurations
"docker", # Kept to not break existing configurations
]
VALID_IMAGES: Final = [

View File

@@ -0,0 +1,56 @@
"""Provides diagnostics for Version."""
from __future__ import annotations
from typing import Any
from attr import asdict
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import DOMAIN
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
devices = []
registry_devices = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
for device in registry_devices:
entities = []
registry_entities = er.async_entries_for_device(
entity_registry,
device_id=device.id,
include_disabled_entities=True,
)
for entity in registry_entities:
state_dict = None
if state := hass.states.get(entity.entity_id):
state_dict = dict(state.as_dict())
state_dict.pop("context", None)
entities.append({"entry": asdict(entity), "state": state_dict})
devices.append({"device": asdict(device), "entities": entities})
return {
"entry": config_entry.as_dict(),
"coordinator_data": {
"version": coordinator.version,
"version_data": coordinator.version_data,
},
"devices": devices,
}

View File

@@ -133,6 +133,7 @@ APPLICATION_VERSION = "application_version"
MAX_INCLUSION_REQUEST_INTERVAL = "max_inclusion_request_interval"
UUID = "uuid"
SUPPORTED_PROTOCOLS = "supported_protocols"
ADDITIONAL_PROPERTIES = "additional_properties"
FEATURE = "feature"
UNPROVISION = "unprovision"
@@ -170,6 +171,7 @@ def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation
max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL),
uuid=info.get(UUID),
supported_protocols=protocols if protocols else None,
additional_properties=info.get(ADDITIONAL_PROPERTIES, {}),
)
return info
@@ -212,6 +214,7 @@ QR_PROVISIONING_INFORMATION_SCHEMA = vol.All(
cv.ensure_list,
[vol.Coerce(Protocols)],
),
vol.Optional(ADDITIONAL_PROPERTIES): dict,
}
),
convert_qr_provisioning_information,

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "6"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@@ -11,7 +11,7 @@ import logging
import math
import sys
from timeit import default_timer as timer
from typing import Any, Final, Literal, TypedDict, final
from typing import Any, Literal, TypedDict, final
import voluptuous as vol
@@ -58,7 +58,13 @@ SOURCE_PLATFORM_CONFIG = "platform_config"
FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1
ENTITY_CATEGORIES_SCHEMA: Final = vol.In(ENTITY_CATEGORIES)
def validate_entity_category(value: Any | None) -> EntityCategory:
"""Validate entity category configuration."""
value = vol.In(ENTITY_CATEGORIES)(value)
return EntityCategory(value)
ENTITY_CATEGORIES_SCHEMA = validate_entity_category
@callback
@@ -217,7 +223,10 @@ def convert_to_entity_category(
"EntityCategory instead" % (type(value).__name__, value),
error_if_core=False,
)
return EntityCategory(value)
try:
return EntityCategory(value)
except ValueError:
return None
return value

View File

@@ -20,7 +20,7 @@ httpx==0.21.3
ifaddr==0.1.7
jinja2==3.0.3
paho-mqtt==1.6.1
pillow==9.0.0
pillow==9.0.1
pip>=8.0.3,<20.3
pyserial==3.5
python-slugify==4.0.1

View File

@@ -472,7 +472,10 @@ def color_rgbww_to_rgb(
except ZeroDivisionError:
ct_ratio = 0.5
color_temp_mired = min_mireds + ct_ratio * mired_range
color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
if color_temp_mired:
color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
else:
color_temp_kelvin = 0
w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin)
white_level = max(cw, ww) / 255

View File

@@ -138,7 +138,7 @@ aio_georss_gdacs==0.5
aioambient==2021.11.0
# homeassistant.components.aseko_pool_live
aioaseko==0.0.1
aioaseko==0.0.2
# homeassistant.components.asuswrt
aioasuswrt==1.4.0
@@ -166,7 +166,7 @@ aioeagle==1.1.0
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==10.8.1
aioesphomeapi==10.8.2
# homeassistant.components.flo
aioflo==2021.11.0
@@ -254,7 +254,7 @@ aioridwell==2021.12.2
aiosenseme==0.6.1
# homeassistant.components.shelly
aioshelly==1.0.8
aioshelly==1.0.9
# homeassistant.components.steamist
aiosteamist==0.3.1
@@ -308,7 +308,7 @@ amberelectric==1.0.3
ambiclimate==0.2.1
# homeassistant.components.amcrest
amcrest==1.9.3
amcrest==1.9.4
# homeassistant.components.androidtv
androidtv[async]==0.0.63
@@ -681,7 +681,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.28.20
flux_led==0.28.22
# homeassistant.components.homekit
fnvhash==0.1.0
@@ -764,7 +764,7 @@ google-cloud-pubsub==2.9.0
google-cloud-texttospeech==0.4.0
# homeassistant.components.nest
google-nest-sdm==1.6.0
google-nest-sdm==1.7.1
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@@ -914,7 +914,7 @@ influxdb-client==1.24.0
influxdb==5.3.1
# homeassistant.components.intellifire
intellifire4py==0.5
intellifire4py==0.6
# homeassistant.components.iotawatt
iotawattpy==0.1.0
@@ -1049,7 +1049,7 @@ minio==5.0.10
mitemp_bt==0.0.5
# homeassistant.components.motion_blinds
motionblinds==0.5.10
motionblinds==0.5.12
# homeassistant.components.motioneye
motioneye-client==0.3.12
@@ -1249,13 +1249,13 @@ pilight==0.1.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
pillow==9.0.0
pillow==9.0.1
# homeassistant.components.dominos
pizzapi==0.0.3
# homeassistant.components.plex
plexapi==4.9.1
plexapi==4.9.2
# homeassistant.components.plex
plexauth==0.0.6
@@ -1310,7 +1310,7 @@ pushbullet.py==0.11.0
pushover_complete==1.1.1
# homeassistant.components.pvoutput
pvo==0.2.1
pvo==0.2.2
# homeassistant.components.rpi_gpio_pwm
pwmled==1.6.7
@@ -1331,7 +1331,7 @@ py-nightscout==1.2.2
py-schluter==0.1.7
# homeassistant.components.synology_dsm
py-synologydsm-api==1.0.5
py-synologydsm-api==1.0.6
# homeassistant.components.zabbix
py-zabbix==1.1.7
@@ -1500,7 +1500,7 @@ pyeight==0.2.0
pyemby==1.8
# homeassistant.components.envisalink
pyenvisalink==4.3
pyenvisalink==4.4
# homeassistant.components.ephember
pyephember==0.3.1
@@ -1954,7 +1954,7 @@ python-mpd2==3.0.4
python-mystrom==1.1.2
# homeassistant.components.nest
python-nest==4.1.0
python-nest==4.2.0
# homeassistant.components.ozw
python-openzwave-mqtt[mqtt-client]==1.4.0
@@ -2190,7 +2190,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==2022.01.0
simplisafe-python==2022.02.1
# homeassistant.components.sisyphus
sisyphus-control==3.1.2
@@ -2228,7 +2228,7 @@ smhi-pkg==1.0.15
snapcast==2.1.3
# homeassistant.components.sonos
soco==0.26.0
soco==0.26.2
# homeassistant.components.solaredge_local
solaredge-local==0.2.0
@@ -2496,7 +2496,7 @@ xbox-webapi==2.0.11
xboxapi==2.0.1
# homeassistant.components.knx
xknx==0.19.1
xknx==0.19.2
# homeassistant.components.bluesound
# homeassistant.components.fritz
@@ -2513,7 +2513,7 @@ xs1-api-client==3.0.0
yalesmartalarmclient==0.3.7
# homeassistant.components.august
yalexs==1.1.20
yalexs==1.1.22
# homeassistant.components.yeelight
yeelight==0.7.8

View File

@@ -91,7 +91,7 @@ aio_georss_gdacs==0.5
aioambient==2021.11.0
# homeassistant.components.aseko_pool_live
aioaseko==0.0.1
aioaseko==0.0.2
# homeassistant.components.asuswrt
aioasuswrt==1.4.0
@@ -119,7 +119,7 @@ aioeagle==1.1.0
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==10.8.1
aioesphomeapi==10.8.2
# homeassistant.components.flo
aioflo==2021.11.0
@@ -189,7 +189,7 @@ aioridwell==2021.12.2
aiosenseme==0.6.1
# homeassistant.components.shelly
aioshelly==1.0.8
aioshelly==1.0.9
# homeassistant.components.steamist
aiosteamist==0.3.1
@@ -427,7 +427,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.28.20
flux_led==0.28.22
# homeassistant.components.homekit
fnvhash==0.1.0
@@ -492,7 +492,7 @@ google-api-python-client==1.6.4
google-cloud-pubsub==2.9.0
# homeassistant.components.nest
google-nest-sdm==1.6.0
google-nest-sdm==1.7.1
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@@ -586,7 +586,7 @@ influxdb-client==1.24.0
influxdb==5.3.1
# homeassistant.components.intellifire
intellifire4py==0.5
intellifire4py==0.6
# homeassistant.components.iotawatt
iotawattpy==0.1.0
@@ -655,7 +655,7 @@ millheater==0.9.0
minio==5.0.10
# homeassistant.components.motion_blinds
motionblinds==0.5.10
motionblinds==0.5.12
# homeassistant.components.motioneye
motioneye-client==0.3.12
@@ -771,10 +771,10 @@ pilight==0.1.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
pillow==9.0.0
pillow==9.0.1
# homeassistant.components.plex
plexapi==4.9.1
plexapi==4.9.2
# homeassistant.components.plex
plexauth==0.0.6
@@ -814,7 +814,7 @@ pure-python-adb[async]==0.3.0.dev0
pushbullet.py==0.11.0
# homeassistant.components.pvoutput
pvo==0.2.1
pvo==0.2.2
# homeassistant.components.canary
py-canary==0.5.1
@@ -829,7 +829,7 @@ py-melissa-climate==2.1.4
py-nightscout==1.2.2
# homeassistant.components.synology_dsm
py-synologydsm-api==1.0.5
py-synologydsm-api==1.0.6
# homeassistant.components.seventeentrack
py17track==2021.12.2
@@ -1206,7 +1206,7 @@ python-kasa==0.4.1
python-miio==0.5.9.2
# homeassistant.components.nest
python-nest==4.1.0
python-nest==4.2.0
# homeassistant.components.ozw
python-openzwave-mqtt[mqtt-client]==1.4.0
@@ -1337,7 +1337,7 @@ sharkiqpy==0.1.8
simplehound==0.3
# homeassistant.components.simplisafe
simplisafe-python==2022.01.0
simplisafe-python==2022.02.1
# homeassistant.components.slack
slackclient==2.5.0
@@ -1355,7 +1355,7 @@ smarthab==0.21
smhi-pkg==1.0.15
# homeassistant.components.sonos
soco==0.26.0
soco==0.26.2
# homeassistant.components.solaredge
solaredge==0.0.2
@@ -1527,7 +1527,7 @@ wolf_smartset==0.1.11
xbox-webapi==2.0.11
# homeassistant.components.knx
xknx==0.19.1
xknx==0.19.2
# homeassistant.components.bluesound
# homeassistant.components.fritz
@@ -1541,7 +1541,7 @@ xmltodict==0.12.0
yalesmartalarmclient==0.3.7
# homeassistant.components.august
yalexs==1.1.20
yalexs==1.1.22
# homeassistant.components.yeelight
yeelight==0.7.8

View File

@@ -1,6 +1,6 @@
[metadata]
name = homeassistant
version = 2022.2.2
version = 2022.2.6
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -1,13 +1,17 @@
"""Define patches used for androidtv tests."""
from unittest.mock import mock_open, patch
from androidtv.constants import CMD_DEVICE_PROPERTIES, CMD_MAC_ETH0, CMD_MAC_WLAN0
KEY_PYTHON = "python"
KEY_SERVER = "server"
ADB_DEVICE_TCP_ASYNC_FAKE = "AdbDeviceTcpAsyncFake"
DEVICE_ASYNC_FAKE = "DeviceAsyncFake"
PROPS_DEV_INFO = "fake\nfake\n0123456\nfake"
PROPS_DEV_MAC = "ether ab:cd:ef:gh:ij:kl brd"
class AdbDeviceTcpAsyncFake:
"""A fake of the `adb_shell.adb_device_async.AdbDeviceTcpAsync` class."""
@@ -100,12 +104,18 @@ def patch_connect(success):
}
def patch_shell(response=None, error=False):
def patch_shell(response=None, error=False, mac_eth=False):
"""Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods."""
async def shell_success(self, cmd, *args, **kwargs):
"""Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods when they are successful."""
self.shell_cmd = cmd
if cmd == CMD_DEVICE_PROPERTIES:
return PROPS_DEV_INFO
if cmd == CMD_MAC_WLAN0:
return PROPS_DEV_MAC
if cmd == CMD_MAC_ETH0:
return PROPS_DEV_MAC if mac_eth else None
return response
async def shell_fail_python(self, cmd, *args, **kwargs):
@@ -185,15 +195,3 @@ PATCH_ANDROIDTV_UPDATE_EXCEPTION = patch(
"androidtv.androidtv.androidtv_async.AndroidTVAsync.update",
side_effect=ZeroDivisionError,
)
PATCH_DEVICE_PROPERTIES = patch(
"androidtv.basetv.basetv_async.BaseTVAsync.get_device_properties",
return_value={
"manufacturer": "a",
"model": "b",
"serialno": "c",
"sw_version": "d",
"wifimac": "ab:cd:ef:gh:ij:kl",
"ethmac": None,
},
)

View File

@@ -31,6 +31,7 @@ from homeassistant.components.androidtv.const import (
DEFAULT_PORT,
DOMAIN,
PROP_ETHMAC,
PROP_WIFIMAC,
)
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
@@ -42,6 +43,7 @@ from tests.components.androidtv.patchers import isfile
ADBKEY = "adbkey"
ETH_MAC = "a1:b1:c1:d1:e1:f1"
WIFI_MAC = "a2:b2:c2:d2:e2:f2"
HOST = "127.0.0.1"
VALID_DETECT_RULE = [{"paused": {"media_session_state": 3}}]
@@ -84,18 +86,28 @@ PATCH_SETUP_ENTRY = patch(
class MockConfigDevice:
"""Mock class to emulate Android TV device."""
def __init__(self, eth_mac=ETH_MAC):
def __init__(self, eth_mac=ETH_MAC, wifi_mac=None):
"""Initialize a fake device to test config flow."""
self.available = True
self.device_properties = {PROP_ETHMAC: eth_mac}
self.device_properties = {PROP_ETHMAC: eth_mac, PROP_WIFIMAC: wifi_mac}
async def adb_close(self):
"""Fake method to close connection."""
self.available = False
@pytest.mark.parametrize("config", [CONFIG_PYTHON_ADB, CONFIG_ADB_SERVER])
async def test_user(hass, config):
@pytest.mark.parametrize(
["config", "eth_mac", "wifi_mac"],
[
(CONFIG_PYTHON_ADB, ETH_MAC, None),
(CONFIG_ADB_SERVER, ETH_MAC, None),
(CONFIG_PYTHON_ADB, None, WIFI_MAC),
(CONFIG_ADB_SERVER, None, WIFI_MAC),
(CONFIG_PYTHON_ADB, ETH_MAC, WIFI_MAC),
(CONFIG_ADB_SERVER, ETH_MAC, WIFI_MAC),
],
)
async def test_user(hass, config, eth_mac, wifi_mac):
"""Test user config."""
flow_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True}
@@ -106,7 +118,7 @@ async def test_user(hass, config):
# test with all provided
with patch(
CONNECT_METHOD,
return_value=(MockConfigDevice(), None),
return_value=(MockConfigDevice(eth_mac, wifi_mac), None),
), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP:
result = await hass.config_entries.flow.async_configure(
flow_result["flow_id"], user_input=config
@@ -273,7 +285,7 @@ async def test_invalid_serial(hass):
"""Test for invalid serialno."""
with patch(
CONNECT_METHOD,
return_value=(MockConfigDevice(eth_mac=""), None),
return_value=(MockConfigDevice(eth_mac=None), None),
), PATCH_GET_HOST_IP:
result = await hass.config_entries.flow.async_init(
DOMAIN,

View File

@@ -142,29 +142,6 @@ def _setup(config):
return patch_key, entity_id, config_entry
async def test_setup_with_properties(hass):
"""Test that setup succeeds with device properties.
the response must be a string with the following info separated with line break:
"manufacturer, model, serialno, version, mac_wlan0_output, mac_eth0_output"
"""
patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER)
config_entry.add_to_hass(hass)
response = "fake\nfake\n0123456\nfake\nether a1:b1:c1:d1:e1:f1 brd\nnone"
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(response)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
@pytest.mark.parametrize(
"config",
[
@@ -190,9 +167,8 @@ async def test_reconnect(hass, caplog, config):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
@@ -259,9 +235,8 @@ async def test_adb_shell_returns_none(hass, config):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
@@ -289,9 +264,8 @@ async def test_setup_with_adbkey(hass):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key
], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, PATCH_ISFILE, PATCH_ACCESS:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
@@ -324,9 +298,8 @@ async def test_sources(hass, config0):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
@@ -404,9 +377,8 @@ async def _test_exclude_sources(hass, config0, expected_sources):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
@@ -486,9 +458,8 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch)
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
@@ -714,9 +685,8 @@ async def test_setup_fail(hass, config):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) is False
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id) is False
await hass.async_block_till_done()
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)
@@ -733,9 +703,8 @@ async def test_adb_command(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch(
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response
@@ -763,9 +732,8 @@ async def test_adb_command_unicode_decode_error(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch(
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell",
@@ -793,9 +761,8 @@ async def test_adb_command_key(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch(
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response
@@ -823,9 +790,8 @@ async def test_adb_command_get_properties(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch(
"androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties_dict",
@@ -853,9 +819,8 @@ async def test_learn_sendevent(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch(
"androidtv.basetv.basetv_async.BaseTVAsync.learn_sendevent",
@@ -882,9 +847,8 @@ async def test_update_lock_not_acquired(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
await hass.helpers.entity_component.async_update_entity(entity_id)
@@ -918,9 +882,8 @@ async def test_download(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Failed download because path is not whitelisted
with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_pull") as patch_pull:
@@ -965,9 +928,8 @@ async def test_upload(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Failed upload because path is not whitelisted
with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_push") as patch_push:
@@ -1010,9 +972,8 @@ async def test_androidtv_volume_set(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch(
"androidtv.basetv.basetv_async.BaseTVAsync.set_volume_level", return_value=0.5
@@ -1038,9 +999,8 @@ async def test_get_image(hass, hass_ws_client):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patchers.patch_shell("11")[patch_key]:
await hass.helpers.entity_component.async_update_entity(entity_id)
@@ -1115,9 +1075,8 @@ async def test_services_androidtv(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
await _test_service(
@@ -1162,9 +1121,8 @@ async def test_services_firetv(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
await _test_service(hass, entity_id, SERVICE_MEDIA_STOP, "back")
@@ -1179,9 +1137,8 @@ async def test_volume_mute(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
service_data = {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_MUTED: True}
@@ -1224,9 +1181,8 @@ async def test_connection_closed_on_ha_stop(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch(
"androidtv.androidtv.androidtv_async.AndroidTVAsync.adb_close"
@@ -1249,9 +1205,8 @@ async def test_exception(hass):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await hass.helpers.entity_component.async_update_entity(entity_id)
state = hass.states.get(entity_id)

View File

@@ -595,7 +595,7 @@ async def test_entity_availability(hass: HomeAssistant):
conn_status_cb(connection_status)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == "idle"
assert state.state == "off"
connection_status = MagicMock()
connection_status.status = "DISCONNECTED"
@@ -624,7 +624,7 @@ async def test_entity_cast_status(hass: HomeAssistant):
state = hass.states.get(entity_id)
assert state is not None
assert state.name == "Speaker"
assert state.state == "idle"
assert state.state == "off"
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
# No media status, pause, play, stop not supported
@@ -642,8 +642,8 @@ async def test_entity_cast_status(hass: HomeAssistant):
cast_status_cb(cast_status)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
# Volume not hidden even if no app is active
assert state.attributes.get("volume_level") == 0.5
# Volume hidden if no app is active
assert state.attributes.get("volume_level") is None
assert not state.attributes.get("is_volume_muted")
chromecast.app_id = "1234"
@@ -747,7 +747,7 @@ async def test_supported_features(
state = hass.states.get(entity_id)
assert state is not None
assert state.name == "Speaker"
assert state.state == "idle"
assert state.state == "off"
assert state.attributes.get("supported_features") == supported_features_no_media
media_status = MagicMock(images=None)
@@ -882,7 +882,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock):
state = hass.states.get(entity_id)
assert state is not None
assert state.name == "Speaker"
assert state.state == "idle"
assert state.state == "off"
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
# Play_media
@@ -928,7 +928,7 @@ async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock):
state = hass.states.get(entity_id)
assert state is not None
assert state.name == "Speaker"
assert state.state == "idle"
assert state.state == "off"
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
# Play_media - cast with app ID
@@ -970,7 +970,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock):
state = hass.states.get(entity_id)
assert state is not None
assert state.name == "Speaker"
assert state.state == "idle"
assert state.state == "off"
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
# play_media - media_type cast with invalid JSON
@@ -1042,7 +1042,7 @@ async def test_entity_media_content_type(hass: HomeAssistant):
state = hass.states.get(entity_id)
assert state is not None
assert state.name == "Speaker"
assert state.state == "idle"
assert state.state == "off"
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
media_status = MagicMock(images=None)
@@ -1213,7 +1213,7 @@ async def test_entity_media_states(hass: HomeAssistant, app_id, state_no_media):
state = hass.states.get(entity_id)
assert state is not None
assert state.name == "Speaker"
assert state.state == "idle"
assert state.state == "off"
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
# App id updated, but no media status
@@ -1258,7 +1258,7 @@ async def test_entity_media_states(hass: HomeAssistant, app_id, state_no_media):
cast_status_cb(cast_status)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == "idle"
assert state.state == "off"
# No cast status
chromecast.is_idle = False
@@ -1286,7 +1286,7 @@ async def test_entity_media_states_lovelace_app(hass: HomeAssistant):
state = hass.states.get(entity_id)
assert state is not None
assert state.name == "Speaker"
assert state.state == "idle"
assert state.state == "off"
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
chromecast.app_id = CAST_APP_ID_HOMEASSISTANT_LOVELACE
@@ -1326,7 +1326,7 @@ async def test_entity_media_states_lovelace_app(hass: HomeAssistant):
media_status_cb(media_status)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == "idle"
assert state.state == "off"
chromecast.is_idle = False
media_status_cb(media_status)
@@ -1355,7 +1355,7 @@ async def test_group_media_states(hass, mz_mock):
state = hass.states.get(entity_id)
assert state is not None
assert state.name == "Speaker"
assert state.state == "idle"
assert state.state == "off"
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
group_media_status = MagicMock(images=None)
@@ -1406,7 +1406,7 @@ async def test_group_media_control(hass, mz_mock, quick_play_mock):
state = hass.states.get(entity_id)
assert state is not None
assert state.name == "Speaker"
assert state.state == "idle"
assert state.state == "off"
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
group_media_status = MagicMock(images=None)

View File

@@ -61,3 +61,25 @@ async def test_sensor(
assert state.attributes.get(ATTR_ARCH) == "aargh"
assert state.attributes.get(ATTR_BRAND) == "Intel Ryzen 7"
assert state.attributes.get(ATTR_HZ) == 3.6
async def test_sensor_partial_info(
hass: HomeAssistant,
mock_cpuinfo: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the CPU Speed sensor missing info."""
mock_config_entry.add_to_hass(hass)
# Pop some info from the mocked CPUSpeed
mock_cpuinfo.return_value.pop("brand_raw")
mock_cpuinfo.return_value.pop("arch_string_raw")
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.cpu_speed")
assert state
assert state.state == "3.2"
assert state.attributes.get(ATTR_ARCH) is None
assert state.attributes.get(ATTR_BRAND) is None

View File

@@ -255,6 +255,33 @@ async def test_dhcp_match_macaddress(hass):
)
async def test_dhcp_multiple_match_only_one_flow(hass):
"""Test matching the domain multiple times only generates one flow."""
integration_matchers = [
{"domain": "mock-domain", "macaddress": "B8B7F1*"},
{"domain": "mock-domain", "hostname": "connect"},
]
packet = Ether(RAW_DHCP_REQUEST)
async_handle_dhcp_packet = await _async_get_handle_dhcp_packet(
hass, integration_matchers
)
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
await async_handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 1
assert mock_init.mock_calls[0][1][0] == "mock-domain"
assert mock_init.mock_calls[0][2]["context"] == {
"source": config_entries.SOURCE_DHCP
}
assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo(
ip="192.168.210.56",
hostname="connect",
macaddress="b8b7f16db533",
)
async def test_dhcp_match_macaddress_without_hostname(hass):
"""Test matching based on macaddress only."""
integration_matchers = [{"domain": "mock-domain", "macaddress": "606BBD*"}]

View File

@@ -9,6 +9,7 @@ import pytest
import respx
from homeassistant import config as hass_config
from homeassistant.components.camera import async_get_mjpeg_stream
from homeassistant.components.generic import DOMAIN
from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.const import SERVICE_RELOAD
@@ -515,3 +516,29 @@ async def test_no_still_image_url(hass, hass_client):
mock_stream.async_get_image.assert_called_once()
assert resp.status == HTTPStatus.OK
assert await resp.read() == b"stream_keyframe_image"
async def test_frame_interval_property(hass):
"""Test that the frame interval is calculated and returned correctly."""
await async_setup_component(
hass,
"camera",
{
"camera": {
"name": "config_test",
"platform": "generic",
"stream_source": "rtsp://example.com:554/rtsp/",
"framerate": 5,
},
},
)
await hass.async_block_till_done()
request = Mock()
with patch(
"homeassistant.components.camera.async_get_still_stream"
) as mock_get_stream:
await async_get_mjpeg_stream(hass, request, "camera.config_test")
assert mock_get_stream.call_args_list[0][0][3] == pytest.approx(0.2)

View File

@@ -1,6 +1,7 @@
"""The tests for the group cover platform."""
from datetime import timedelta
import async_timeout
import pytest
from homeassistant.components.cover import (
@@ -735,3 +736,52 @@ async def test_is_opening_closing(hass, setup_comp):
assert hass.states.get(DEMO_COVER_TILT).state == STATE_OPENING
assert hass.states.get(DEMO_COVER_POS).state == STATE_OPEN
assert hass.states.get(COVER_GROUP).state == STATE_OPENING
async def test_nested_group(hass):
"""Test nested cover group."""
await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{"platform": "demo"},
{
"platform": "group",
"entities": ["cover.bedroom_group"],
"name": "Nested Group",
},
{
"platform": "group",
CONF_ENTITIES: [DEMO_COVER_POS, DEMO_COVER_TILT],
"name": "Bedroom Group",
},
]
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
state = hass.states.get("cover.bedroom_group")
assert state is not None
assert state.state == STATE_OPEN
assert state.attributes.get(ATTR_ENTITY_ID) == [DEMO_COVER_POS, DEMO_COVER_TILT]
state = hass.states.get("cover.nested_group")
assert state is not None
assert state.state == STATE_OPEN
assert state.attributes.get(ATTR_ENTITY_ID) == ["cover.bedroom_group"]
# Test controlling the nested group
async with async_timeout.timeout(0.5):
await hass.services.async_call(
DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: "cover.nested_group"},
blocking=True,
)
assert hass.states.get(DEMO_COVER_POS).state == STATE_CLOSING
assert hass.states.get(DEMO_COVER_TILT).state == STATE_CLOSING
assert hass.states.get("cover.bedroom_group").state == STATE_CLOSING
assert hass.states.get("cover.nested_group").state == STATE_CLOSING

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