Compare commits

..

45 Commits

Author SHA1 Message Date
Franck Nijhof
7fc36c4fe0 Merge pull request #62182 from home-assistant/rc 2021-12-17 13:43:37 +01:00
Franck Nijhof
5196a770cc Bumped version to 2021.12.3 2021-12-17 11:43:38 +01:00
Erik Montnemery
54d7380f4d Fix threading error in zha (#62170) 2021-12-17 11:43:04 +01:00
Erik Montnemery
c445e93d45 Fix threading error in scripts with repeat or choose actions (#62168) 2021-12-17 11:43:00 +01:00
Marcel van der Veldt
614529d7c3 Add guard in call to activate_scene in Hue (#62177) 2021-12-17 11:41:04 +01:00
Allen Porter
9361c9ef60 Bump google-nest-sdm to 0.4.9 (#62160) 2021-12-17 11:41:00 +01:00
J. Nick Koston
19a0644b50 Fix Non-thread-safe operation in logbook (#62148) 2021-12-17 11:40:57 +01:00
J. Nick Koston
82173f477c Fix Non-thread-safe operation in homekit light events (#62147) 2021-12-17 11:40:54 +01:00
Simone Chemelli
b4af32624d Improve availability for Shelly Valve (#62129) 2021-12-17 11:40:50 +01:00
Maximilian
78f40bd4bf Add missing timezone information (#62106) 2021-12-17 11:40:47 +01:00
Erik Montnemery
d92ad76ed9 Fix none-check in template light (#62089) 2021-12-17 11:40:44 +01:00
J. Nick Koston
e44d50e1b1 Bump flux_led to 0.26.15 (#62017) 2021-12-17 11:40:40 +01:00
Eduard van Valkenburg
95c0eeecfb Brunt dependency bump to 1.0.2 (#62014) 2021-12-17 11:40:37 +01:00
Marcel van der Veldt
ec263840ba Bump aiohue to 3.0.6 (#61974) 2021-12-17 11:40:34 +01:00
Marvin Wichmann
8047134c88 Fix notify platform setup for KNX (#61842)
* Fix notify platform setup for KNX

* Apply review suggestions

* Store hass config in DATA_HASS_CONFIG

* Readd guard clause
2021-12-17 11:40:31 +01:00
epenet
cb89688873 Fix OwnetError preventing onewire initialisation (#61696)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2021-12-17 11:40:27 +01:00
Simone Chemelli
c73319e162 Add restore logic to Shelly climate platform (#61632)
* Add restore logic to Shelly climate platform

* Handle missing channel on restore
2021-12-17 11:39:47 +01:00
Ian
499cc2e51d Nextbus upcoming sort as integer (#61416) 2021-12-17 11:33:21 +01:00
sindudas
5a03fffc20 Update ebusdpy version (#59899) 2021-12-17 11:33:17 +01:00
Franck Nijhof
6d8d472f0f Merge pull request #61902 from home-assistant/rc 2021-12-15 17:02:35 +01:00
Franck Nijhof
ac2897fc67 Bumped version to 2021.12.2 2021-12-15 16:04:48 +01:00
Bram Kragten
e7e20533bd Update frontend to 20211215.0 (#61877) 2021-12-15 16:03:37 +01:00
Marcel van der Veldt
2772bae2e1 Bump aiohue to 3.0.5 (#61875) 2021-12-15 16:03:34 +01:00
Allen Porter
86622794e0 Bump google-nest-sdm to 0.4.8 (#61851) 2021-12-15 16:03:30 +01:00
Michael Davie
686f6768fc Fix broken Environment Canada (#61848) 2021-12-15 16:03:27 +01:00
Marvin Wichmann
f271fea07c Allow setting local_ip for knx routing connections (#61836) 2021-12-15 16:03:24 +01:00
Aaron Bach
77b1df5902 Ensure SimpliSafe websocket reconnects upon new token (#61835) 2021-12-15 16:03:20 +01:00
Teemu R
1faa111222 Bump python-miio to 0.5.9.2 (#61831) 2021-12-15 16:03:17 +01:00
Daniel Hjelseth Høyer
b513301363 Tibber, update library, fixes #61525 (#61813) 2021-12-15 16:03:14 +01:00
Erik Montnemery
32bdcdd663 Bump pychromecast to 10.2.1 (#61811) 2021-12-15 16:03:11 +01:00
Erik Montnemery
40f76d4ed9 Don't override pychromecast MediaController's APP ID (#61796) 2021-12-15 16:03:07 +01:00
MattWestb
34568aad89 Fix ZHA unoccupied setpoints. (#61791)
ATTR_UNOCCP_HEAT_SETPT and ATTR_UNOCCP_COOL_SETPT is mixed up. 
Fixing so heating is heating and cooling is colling.
2021-12-15 16:03:04 +01:00
Eduard van Valkenburg
ffe84e8ece Bump brunt package to 1.0.1 (#61784) 2021-12-15 16:03:01 +01:00
Franck Nijhof
8cbd89282b Upgrade tailscale to 0.1.5 (#61744) 2021-12-15 16:02:58 +01:00
Marcel van der Veldt
1467668c94 Blacklist availability check for a light at startup in Hue integration (#61737) 2021-12-15 16:02:55 +01:00
Marcel van der Veldt
bbef38964d Fix Flash effect for Hue lights (#61733) 2021-12-15 16:02:52 +01:00
Marcel van der Veldt
03b88af032 Fix turn_off with transition for grouped Hue lights (#61728)
* fix turn_off with transition for grouped hue lights

* add test
2021-12-15 16:02:49 +01:00
Marcel van der Veldt
0626bc8b4f Add check for incompatible device trigger in Hue integration (#61726) 2021-12-15 16:02:46 +01:00
Vilppu Vuorinen
37ecbc53a7 Update pymelcloud to 2.5.6 (#61717) 2021-12-15 16:02:43 +01:00
Paulus Schoutsen
52c96654a4 Bump aiohue to 3.0.4 (#61709) 2021-12-15 16:02:39 +01:00
Joakim Sørensen
791c2f4b8a Add additional-tag to machine builds (#61693) 2021-12-15 16:02:36 +01:00
Austin Mroczek
ed041d5b7c Bump total_connect_client to 2021.12 (#61634) 2021-12-15 16:02:33 +01:00
Allen Porter
1833ab96dc Suppress errors for legacy nest api when using media source (#61629) 2021-12-15 16:02:29 +01:00
majuss
ff2e2656b3 Upgrade lupupy to 0.0.24 (#61598) 2021-12-15 16:02:26 +01:00
bsmappee
599c20c76e Bump pysmappee to 0.2.29 (#61160) 2021-12-15 16:02:19 +01:00
54 changed files with 793 additions and 162 deletions

View File

@@ -131,7 +131,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2021.11.4
uses: home-assistant/builder@2021.12.0
with:
args: |
$BUILD_ARGS \
@@ -170,6 +170,17 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v2.4.0
- name: Set build additional args
run: |
# Create general tags
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
else
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
fi
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
with:
@@ -184,7 +195,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2021.11.4
uses: home-assistant/builder@2021.12.0
with:
args: |
$BUILD_ARGS \

View File

@@ -3,7 +3,7 @@
"name": "Brunt Blind Engine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brunt",
"requirements": ["brunt==1.0.0"],
"requirements": ["brunt==1.0.2"],
"codeowners": ["@eavanvalkenburg"],
"iot_class": "cloud_polling"
}

View File

@@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==10.1.1"],
"requirements": ["pychromecast==10.2.1"],
"after_dependencies": [
"cloud",
"http",

View File

@@ -47,7 +47,6 @@ from homeassistant.components.plex.const import PLEX_URI_SCHEME
from homeassistant.components.plex.services import lookup_plex_media
from homeassistant.const import (
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
CAST_APP_ID_HOMEASSISTANT_MEDIA,
EVENT_HOMEASSISTANT_STOP,
STATE_IDLE,
STATE_OFF,
@@ -230,7 +229,6 @@ class CastDevice(MediaPlayerEntity):
self._cast_info.cast_info,
ChromeCastZeroconf.get_zeroconf(),
)
chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA
self._chromecast = chromecast
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
@@ -527,9 +525,8 @@ class CastDevice(MediaPlayerEntity):
self._chromecast.register_handler(controller)
controller.play_media(media)
else:
self._chromecast.media_controller.play_media(
media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {})
)
app_data = {"media_id": media_id, "media_type": media_type, **extra}
quick_play(self._chromecast, "homeassistant_media", app_data)
def _media_status(self):
"""
@@ -820,7 +817,6 @@ class DynamicCastGroup:
self._cast_info.cast_info,
ChromeCastZeroconf.get_zeroconf(),
)
chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA
self._chromecast = chromecast
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:

View File

@@ -2,7 +2,7 @@
"domain": "ebusd",
"name": "ebusd",
"documentation": "https://www.home-assistant.io/integrations/ebusd",
"requirements": ["ebusdpy==0.0.16"],
"requirements": ["ebusdpy==0.0.17"],
"codeowners": [],
"iot_class": "local_polling"
}

View File

@@ -2,7 +2,7 @@
"domain": "environment_canada",
"name": "Environment Canada",
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"requirements": ["env_canada==0.5.18"],
"requirements": ["env_canada==0.5.20"],
"codeowners": ["@gwww", "@michaeldavie"],
"config_flow": true,
"iot_class": "cloud_polling"

View File

@@ -94,6 +94,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
firmware_date=None,
model_info=None,
model_description=None,
remote_access_enabled=None,
remote_access_host=None,
remote_access_port=None,
)
return await self._async_handle_discovery()
@@ -261,6 +264,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
firmware_date=None,
model_info=None,
model_description=bulb.model_data.description,
remote_access_enabled=None,
remote_access_host=None,
remote_access_port=None,
)

View File

@@ -3,7 +3,7 @@
"name": "Flux LED/MagicHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.26.7"],
"requirements": ["flux_led==0.26.15"],
"quality_scale": "platinum",
"codeowners": ["@icemanch"],
"iot_class": "local_push",

View File

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

View File

@@ -120,10 +120,11 @@ class Light(HomeAccessory):
if self._event_timer:
self._event_timer()
self._event_timer = async_call_later(
self.hass, CHANGE_COALESCE_TIME_WINDOW, self._send_events
self.hass, CHANGE_COALESCE_TIME_WINDOW, self._async_send_events
)
def _send_events(self, *_):
@callback
def _async_send_events(self, *_):
"""Process all changes at once."""
_LOGGER.debug("Coalesced _set_chars: %s", self._pending_events)
char_values = self._pending_events

View File

@@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==3.0.3"],
"requirements": ["aiohue==3.0.6"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@@ -146,8 +146,10 @@ async def hue_activate_scene_v2(
continue
# found match!
if transition:
transition = transition * 100 # in steps of 100ms
await api.scenes.recall(scene.id, dynamic=dynamic, duration=transition)
transition = transition * 1000 # transition is in ms
await bridge.async_request_call(
api.scenes.recall, scene.id, dynamic=dynamic, duration=transition
)
return True
LOGGER.debug(
"Unable to find scene %s for group %s on bridge %s",

View File

@@ -7,6 +7,7 @@ from aiohue.v2.models.button import ButtonEvent
from aiohue.v2.models.resource import ResourceTypes
import voluptuous as vol
from homeassistant.components import persistent_notification
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import event as event_trigger
from homeassistant.const import (
@@ -35,7 +36,7 @@ if TYPE_CHECKING:
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): str,
vol.Required(CONF_SUBTYPE): int,
vol.Required(CONF_SUBTYPE): vol.Union(int, str),
vol.Optional(CONF_UNIQUE_ID): str,
}
)
@@ -54,6 +55,33 @@ DEVICE_SPECIFIC_EVENT_TYPES = {
}
def check_invalid_device_trigger(
bridge: HueBridge,
config: ConfigType,
device_entry: DeviceEntry,
automation_info: AutomationTriggerInfo | None = None,
):
"""Check automation config for deprecated format."""
# NOTE: Remove this check after 2022.6
if isinstance(config["subtype"], int):
return
# found deprecated V1 style trigger, notify the user that it should be adjusted
msg = (
f"Incompatible device trigger detected for "
f"[{device_entry.name}](/config/devices/device/{device_entry.id}) "
"Please manually fix the outdated automation(s) once to fix this issue."
)
if automation_info:
automation_id = automation_info["variables"]["this"]["attributes"]["id"] # type: ignore
msg += f"\n\n[Check it out](/config/automation/edit/{automation_id})."
persistent_notification.async_create(
bridge.hass,
msg,
title="Outdated device trigger found",
notification_id=f"hue_trigger_{device_entry.id}",
)
async def async_validate_trigger_config(
bridge: "HueBridge",
device_entry: DeviceEntry,
@@ -61,6 +89,7 @@ async def async_validate_trigger_config(
):
"""Validate config."""
config = TRIGGER_SCHEMA(config)
check_invalid_device_trigger(bridge, config, device_entry)
return config
@@ -84,6 +113,7 @@ async def async_attach_trigger(
},
}
)
check_invalid_device_trigger(bridge, config, device_entry, automation_info)
return await event_trigger.async_attach_trigger(
hass, event_config, action, automation_info, platform_type="device"
)

View File

@@ -47,6 +47,20 @@ class HueBaseEntity(Entity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.device.id)},
)
# some (3th party) Hue lights report their connection status incorrectly
# causing the zigbee availability to report as disconnected while in fact
# it can be controlled. Although this is in fact something the device manufacturer
# should fix, we work around it here. If the light is reported unavailable at
# startup, we ignore the availability status of the zigbee connection
self._ignore_availability = False
if self.device is None:
return
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
self._ignore_availability = (
# Official Hue lights are reliable
self.device.product_data.manufacturer_name != "Signify Netherlands B.V."
and zigbee.status != ConnectivityServiceStatus.CONNECTED
)
@property
def name(self) -> str:
@@ -98,13 +112,12 @@ class HueBaseEntity(Entity):
def available(self) -> bool:
"""Return entity availability."""
if self.device is None:
# devices without a device attached should be always available
# entities without a device attached should be always available
return True
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
# the zigbee connectivity sensor itself should be always available
return True
if self.device.product_data.manufacturer_name != "Signify Netherlands B.V.":
# availability status for non-philips brand lights is unreliable
if self._ignore_availability:
return True
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
# all device-attached entities get availability from the zigbee connectivity

View File

@@ -6,16 +6,19 @@ from typing import Any
from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
from aiohue.v2.models.feature import AlertEffectType
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_FLASH,
ATTR_TRANSITION,
ATTR_XY_COLOR,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_ONOFF,
COLOR_MODE_XY,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
LightEntity,
)
@@ -32,6 +35,7 @@ ALLOWED_ERRORS = [
'device (groupedLight) is "soft off", command (on) may not have effect',
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"attribute (supportedAlertActions) cannot be written",
]
@@ -88,6 +92,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
self.group = group
self.controller = controller
self.api: HueBridgeV2 = bridge.api
self._attr_supported_features |= SUPPORT_FLASH
self._attr_supported_features |= SUPPORT_TRANSITION
# Entities for Hue groups are disabled by default
@@ -146,6 +151,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
xy_color = kwargs.get(ATTR_XY_COLOR)
color_temp = kwargs.get(ATTR_COLOR_TEMP)
brightness = kwargs.get(ATTR_BRIGHTNESS)
flash = kwargs.get(ATTR_FLASH)
if brightness is not None:
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
@@ -160,6 +166,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
and xy_color is None
and color_temp is None
and transition is None
and flash is None
):
await self.bridge.async_request_call(
self.controller.set_state,
@@ -180,17 +187,37 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
color_xy=xy_color if light.supports_color else None,
color_temp=color_temp if light.supports_color_temperature else None,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=False,
allowed_errors=ALLOWED_ERRORS,
)
transition = kwargs.get(ATTR_TRANSITION)
if transition is not None:
# hue transition duration is in milliseconds
transition = int(transition * 1000)
# NOTE: a grouped_light can only handle turn on/off
# To set other features, you'll have to control the attached lights
if transition is None:
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=False,
allowed_errors=ALLOWED_ERRORS,
)
return
# redirect all other feature commands to underlying lights
for light in self.controller.get_lights(self.resource.id):
await self.bridge.async_request_call(
self.api.lights.set_state,
light.id,
on=False,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
@callback
def on_update(self) -> None:

View File

@@ -6,17 +6,20 @@ from typing import Any
from aiohue import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.lights import LightsController
from aiohue.v2.models.feature import AlertEffectType
from aiohue.v2.models.light import Light
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_FLASH,
ATTR_TRANSITION,
ATTR_XY_COLOR,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_ONOFF,
COLOR_MODE_XY,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
LightEntity,
)
@@ -31,6 +34,7 @@ from .entity import HueBaseEntity
ALLOWED_ERRORS = [
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"attribute (supportedAlertActions) cannot be written",
]
@@ -68,6 +72,7 @@ class HueLight(HueBaseEntity, LightEntity):
) -> None:
"""Initialize the light."""
super().__init__(bridge, controller, resource)
self._attr_supported_features |= SUPPORT_FLASH
self.resource = resource
self.controller = controller
self._supported_color_modes = set()
@@ -154,6 +159,7 @@ class HueLight(HueBaseEntity, LightEntity):
xy_color = kwargs.get(ATTR_XY_COLOR)
color_temp = kwargs.get(ATTR_COLOR_TEMP)
brightness = kwargs.get(ATTR_BRIGHTNESS)
flash = kwargs.get(ATTR_FLASH)
if brightness is not None:
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
@@ -169,12 +175,14 @@ class HueLight(HueBaseEntity, LightEntity):
color_xy=xy_color,
color_temp=color_temp,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
transition = kwargs.get(ATTR_TRANSITION)
flash = kwargs.get(ATTR_FLASH)
if transition is not None:
# hue transition duration is in milliseconds
transition = int(transition * 1000)
@@ -183,5 +191,6 @@ class HueLight(HueBaseEntity, LightEntity):
id=self.resource.id,
on=False,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)

View File

@@ -29,6 +29,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
@@ -44,6 +45,7 @@ from .const import (
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_ROUTING,
CONF_KNX_TUNNELING,
DATA_HASS_CONFIG,
DATA_KNX_CONFIG,
DOMAIN,
KNX_ADDRESS,
@@ -195,6 +197,7 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Start the KNX integration."""
hass.data[DATA_HASS_CONFIG] = config
conf: ConfigType | None = config.get(DOMAIN)
if conf is None:
@@ -251,15 +254,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
hass.config_entries.async_setup_platforms(
entry, [platform for platform in SUPPORTED_PLATFORMS if platform in config]
entry,
[
platform
for platform in SUPPORTED_PLATFORMS
if platform in config and platform is not Platform.NOTIFY
],
)
# set up notify platform, no entry support for notify component yet,
# have to use discovery to load platform.
if NotifySchema.PLATFORM in conf:
# set up notify platform, no entry support for notify component yet
if NotifySchema.PLATFORM in config:
hass.async_create_task(
discovery.async_load_platform(
hass, "notify", DOMAIN, conf[NotifySchema.PLATFORM], config
hass, "notify", DOMAIN, {}, hass.data[DATA_HASS_CONFIG]
)
)
@@ -312,6 +319,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
platform
for platform in SUPPORTED_PLATFORMS
if platform in hass.data[DATA_KNX_CONFIG]
and platform is not Platform.NOTIFY
],
)
if unload_ok:
@@ -383,6 +391,7 @@ class KNXModule:
if _conn_type == CONF_KNX_ROUTING:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING,
local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP),
auto_reconnect=True,
)
if _conn_type == CONF_KNX_TUNNELING:

View File

@@ -137,9 +137,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required(
ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False
): vol.Coerce(bool),
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
}
if self.show_advanced_options:
fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str
return self.async_show_form(
step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors
)
@@ -195,6 +197,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONF_KNX_INDIVIDUAL_ADDRESS: user_input[
CONF_KNX_INDIVIDUAL_ADDRESS
],
ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get(
ConnectionSchema.CONF_KNX_LOCAL_IP
),
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
},
)
@@ -211,6 +216,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
): cv.port,
}
if self.show_advanced_options:
fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str
return self.async_show_form(
step_id="routing", data_schema=vol.Schema(fields), errors=errors
)
@@ -306,7 +314,6 @@ class KNXOptionsFlowHandler(OptionsFlow):
vol.Required(
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
): cv.port,
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
vol.Required(
ConnectionSchema.CONF_KNX_ROUTE_BACK,
default=self.current_config.get(
@@ -381,6 +388,14 @@ class KNXOptionsFlowHandler(OptionsFlow):
}
if self.show_advanced_options:
data_schema[
vol.Optional(
ConnectionSchema.CONF_KNX_LOCAL_IP,
default=self.current_config.get(
ConnectionSchema.CONF_KNX_LOCAL_IP,
),
)
] = str
data_schema[
vol.Required(
ConnectionSchema.CONF_KNX_STATE_UPDATER,

View File

@@ -42,7 +42,10 @@ CONF_STATE_ADDRESS: Final = "state_address"
CONF_SYNC_STATE: Final = "sync_state"
CONF_KNX_INITIAL_CONNECTION_TYPES: Final = [CONF_KNX_TUNNELING, CONF_KNX_ROUTING]
# yaml config merged with config entry data
DATA_KNX_CONFIG: Final = "knx_config"
# original hass yaml config
DATA_HASS_CONFIG: Final = "knx_hass_config"
ATTR_COUNTER: Final = "counter"
ATTR_SOURCE: Final = "source"

View File

@@ -11,7 +11,8 @@ from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN, KNX_ADDRESS
from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS
from .schema import NotifySchema
async def async_get_service(
@@ -20,24 +21,28 @@ async def async_get_service(
discovery_info: DiscoveryInfoType | None = None,
) -> KNXNotificationService | None:
"""Get the KNX notification service."""
if not discovery_info:
if discovery_info is None:
return None
platform_config: dict = discovery_info
xknx: XKNX = hass.data[DOMAIN].xknx
if platform_config := hass.data[DATA_KNX_CONFIG].get(NotifySchema.PLATFORM):
xknx: XKNX = hass.data[DOMAIN].xknx
notification_devices = []
for device_config in platform_config:
notification_devices.append(
XknxNotification(
xknx,
name=device_config[CONF_NAME],
group_address=device_config[KNX_ADDRESS],
notification_devices = []
for device_config in platform_config:
notification_devices.append(
XknxNotification(
xknx,
name=device_config[CONF_NAME],
group_address=device_config[KNX_ADDRESS],
)
)
return (
KNXNotificationService(notification_devices)
if notification_devices
else None
)
return (
KNXNotificationService(notification_devices) if notification_devices else None
)
return None
class KNXNotificationService(BaseNotificationService):

View File

@@ -28,7 +28,8 @@
"data": {
"individual_address": "Individual address for the routing connection",
"multicast_group": "The multicast group used for routing",
"multicast_port": "The multicast port used for routing"
"multicast_port": "The multicast port used for routing",
"local_ip": "Local IP (leave empty if unsure)"
}
}
},
@@ -48,6 +49,7 @@
"individual_address": "Default individual address",
"multicast_group": "Multicast group used for routing and discovery",
"multicast_port": "Multicast port used for routing and discovery",
"local_ip": "Local IP (leave empty if unsure)",
"state_updater": "Globally enable reading states from the KNX Bus",
"rate_limit": "Maximum outgoing telegrams per second"
}
@@ -56,8 +58,7 @@
"data": {
"port": "[%key:common::config_flow::data::port%]",
"host": "[%key:common::config_flow::data::host%]",
"route_back": "Route Back / NAT Mode",
"local_ip": "Local IP (leave empty if unsure)"
"route_back": "Route Back / NAT Mode"
}
}
}

View File

@@ -22,7 +22,8 @@
"data": {
"individual_address": "Individual address for the routing connection",
"multicast_group": "The multicast group used for routing",
"multicast_port": "The multicast port used for routing"
"multicast_port": "The multicast port used for routing",
"local_ip": "Local IP (leave empty if unsure)"
},
"description": "Please configure the routing options."
},
@@ -48,6 +49,7 @@
"individual_address": "Default individual address",
"multicast_group": "Multicast group used for routing and discovery",
"multicast_port": "Multicast port used for routing and discovery",
"local_ip": "Local IP (leave empty if unsure)",
"rate_limit": "Maximum outgoing telegrams per second",
"state_updater": "Globally enable reading states from the KNX Bus"
}
@@ -55,7 +57,6 @@
"tunnel": {
"data": {
"host": "Host",
"local_ip": "Local IP (leave empty if unsure)",
"port": "Port",
"route_back": "Route Back / NAT Mode"
}

View File

@@ -112,6 +112,7 @@ def log_entry(hass, name, message, domain=None, entity_id=None, context=None):
hass.add_job(async_log_entry, hass, name, message, domain, entity_id, context)
@callback
@bind_hass
def async_log_entry(hass, name, message, domain=None, entity_id=None, context=None):
"""Add an entry to the logbook."""

View File

@@ -1,9 +1,8 @@
{
"disabled": "Library has incompatible requirements.",
"domain": "lupusec",
"name": "Lupus Electronics LUPUSEC",
"documentation": "https://www.home-assistant.io/integrations/lupusec",
"requirements": ["lupupy==0.0.21"],
"requirements": ["lupupy==0.0.24"],
"codeowners": ["@majuss"],
"iot_class": "local_polling"
}

View File

@@ -3,7 +3,7 @@
"name": "MELCloud",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/melcloud",
"requirements": ["pymelcloud==2.5.5"],
"requirements": ["pymelcloud==2.5.6"],
"codeowners": ["@vilppuvuorinen"],
"iot_class": "cloud_polling"
}

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==0.4.6"],
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.9"],
"codeowners": ["@allenporter"],
"quality_scale": "platinum",
"dhcp": [

View File

@@ -63,6 +63,9 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]:
"""Return a mapping of device id to eligible Nest event media devices."""
if DATA_SUBSCRIBER not in hass.data[DOMAIN]:
# Integration unloaded, or is legacy nest integration
return {}
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
device_manager = await subscriber.async_get_device_manager()
device_registry = await hass.helpers.device_registry.async_get_registry()

View File

@@ -214,7 +214,7 @@ class NextBusDepartureSensor(SensorEntity):
# Generate list of upcoming times
self._attributes["upcoming"] = ", ".join(
sorted(p["minutes"] for p in predictions)
sorted((p["minutes"] for p in predictions), key=int)
)
latest_prediction = maybe_first(predictions)

View File

@@ -1,6 +1,8 @@
"""The 1-Wire component."""
import logging
from pyownet import protocol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@@ -18,7 +20,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
onewirehub = OneWireHub(hass)
try:
await onewirehub.initialize(entry)
except CannotConnect as exc:
except (
CannotConnect, # Failed to connect to the server
protocol.OwnetError, # Connected to server, but failed to list the devices
) as exc:
raise ConfigEntryNotReady() from exc
hass.data[DOMAIN][entry.entry_id] = onewirehub

View File

@@ -68,13 +68,12 @@ from .utils import (
BLOCK_PLATFORMS: Final = [
"binary_sensor",
"button",
"climate",
"cover",
"light",
"sensor",
"switch",
]
BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"]
BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "climate", "sensor"]
RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"]
_LOGGER: Final = logging.getLogger(__name__)

View File

@@ -3,12 +3,13 @@ from __future__ import annotations
import asyncio
import logging
from types import MappingProxyType
from typing import Any, Final, cast
from aioshelly.block_device import Block
import async_timeout
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity
from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
@@ -20,11 +21,12 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.components.shelly import BlockDeviceWrapper
from homeassistant.components.shelly.entity import ShellyBlockEntity
from homeassistant.components.shelly.utils import get_device_entry_gen
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import device_registry, entity, entity_registry
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
@@ -49,10 +51,30 @@ async def async_setup_entry(
if get_device_entry_gen(config_entry) == 2:
return
wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
][BLOCK]
if wrapper.device.initialized:
await async_setup_climate_entities(async_add_entities, wrapper)
else:
await async_restore_climate_entities(
hass, config_entry, async_add_entities, wrapper
)
async def async_setup_climate_entities(
async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper,
) -> None:
"""Set up online climate devices."""
_LOGGER.info("Setup online climate device %s", wrapper.name)
device_block: Block | None = None
sensor_block: Block | None = None
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
assert wrapper.device.blocks
for block in wrapper.device.blocks:
if block.type == "device":
device_block = block
@@ -60,10 +82,37 @@ async def async_setup_entry(
sensor_block = block
if sensor_block and device_block:
async_add_entities([ShellyClimate(wrapper, sensor_block, device_block)])
async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)])
class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
async def async_restore_climate_entities(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper,
) -> None:
"""Restore sleeping climate devices."""
_LOGGER.info("Setup sleeping climate device %s", wrapper.name)
ent_reg = await entity_registry.async_get_registry(hass)
entries = entity_registry.async_entries_for_config_entry(
ent_reg, config_entry.entry_id
)
for entry in entries:
if entry.domain != CLIMATE_DOMAIN:
continue
_LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain)
async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)])
class BlockSleepingClimate(
RestoreEntity,
ClimateEntity,
entity.Entity,
):
"""Representation of a Shelly climate device."""
_attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT]
@@ -74,45 +123,77 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
_attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"]
_attr_temperature_unit = TEMP_CELSIUS
# pylint: disable=super-init-not-called
def __init__(
self, wrapper: BlockDeviceWrapper, sensor_block: Block, device_block: Block
self,
wrapper: BlockDeviceWrapper,
sensor_block: Block | None,
device_block: Block | None,
entry: entity_registry.RegistryEntry | None = None,
) -> None:
"""Initialize climate."""
super().__init__(wrapper, sensor_block)
self.device_block = device_block
assert self.block.channel
self.wrapper = wrapper
self.block: Block | None = sensor_block
self.control_result: dict[str, Any] | None = None
self.device_block: Block | None = device_block
self.last_state: State | None = None
self.last_state_attributes: MappingProxyType[str, Any]
self._preset_modes: list[str] = []
self._attr_name = self.wrapper.name
self._attr_unique_id = self.wrapper.mac
self._attr_preset_modes: list[str] = [
PRESET_NONE,
*wrapper.device.settings["thermostats"][int(self.block.channel)][
"schedule_profile_names"
],
]
if self.block is not None and self.device_block is not None:
self._unique_id = f"{self.wrapper.mac}-{self.block.description}"
assert self.block.channel
self._preset_modes = [
PRESET_NONE,
*wrapper.device.settings["thermostats"][int(self.block.channel)][
"schedule_profile_names"
],
]
elif entry is not None:
self._unique_id = entry.unique_id
@property
def unique_id(self) -> str:
"""Set unique id of entity."""
return self._unique_id
@property
def name(self) -> str:
"""Name of entity."""
return self.wrapper.name
@property
def should_poll(self) -> bool:
"""If device should be polled."""
return False
@property
def target_temperature(self) -> float | None:
"""Set target temperature."""
return cast(float, self.block.targetTemp)
if self.block is not None:
return cast(float, self.block.targetTemp)
return self.last_state_attributes.get("temperature")
@property
def current_temperature(self) -> float | None:
"""Return current temperature."""
return cast(float, self.block.temp)
if self.block is not None:
return cast(float, self.block.temp)
return self.last_state_attributes.get("current_temperature")
@property
def available(self) -> bool:
"""Device availability."""
return not cast(bool, self.device_block.valveError)
if self.device_block is not None:
return not cast(bool, self.device_block.valveError)
return self.wrapper.last_update_success
@property
def hvac_mode(self) -> str:
"""HVAC current mode."""
if self.device_block is None:
return self.last_state.state if self.last_state else HVAC_MODE_OFF
if self.device_block.mode is None or self._check_is_off():
return HVAC_MODE_OFF
@@ -121,20 +202,45 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
@property
def preset_mode(self) -> str | None:
"""Preset current mode."""
if self.device_block is None:
return self.last_state_attributes.get("preset_mode")
if self.device_block.mode is None:
return None
return self._attr_preset_modes[cast(int, self.device_block.mode)]
return PRESET_NONE
return self._preset_modes[cast(int, self.device_block.mode)]
@property
def hvac_action(self) -> str | None:
"""HVAC current action."""
if self.device_block.status is None or self._check_is_off():
if (
self.device_block is None
or self.device_block.status is None
or self._check_is_off()
):
return CURRENT_HVAC_OFF
return (
CURRENT_HVAC_IDLE if self.device_block.status == "0" else CURRENT_HVAC_HEAT
)
@property
def preset_modes(self) -> list[str]:
"""Preset available modes."""
return self._preset_modes
@property
def device_info(self) -> DeviceInfo:
"""Device info."""
return {
"connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
}
@property
def channel(self) -> str | None:
"""Device channel."""
if self.block is not None:
return self.block.channel
return self.last_state_attributes.get("channel")
def _check_is_off(self) -> bool:
"""Return if valve is off or on."""
return bool(
@@ -148,7 +254,7 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await self.wrapper.device.http_request(
"get", f"thermostat/{self.block.channel}", kwargs
"get", f"thermostat/{self.channel}", kwargs
)
except (asyncio.TimeoutError, OSError) as err:
_LOGGER.error(
@@ -186,3 +292,41 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
await self.set_state_full_path(
schedule=1, schedule_profile=f"{preset_index}"
)
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
_LOGGER.info("Restoring entity %s", self.name)
last_state = await self.async_get_last_state()
if last_state is not None:
self.last_state = last_state
self.last_state_attributes = self.last_state.attributes
self._preset_modes = cast(
list, self.last_state.attributes.get("preset_modes")
)
self.async_on_remove(self.wrapper.async_add_listener(self._update_callback))
async def async_update(self) -> None:
"""Update entity with latest info."""
await self.wrapper.async_request_refresh()
@callback
def _update_callback(self) -> None:
"""Handle device update."""
if not self.wrapper.device.initialized:
self.async_write_ha_state()
return
assert self.wrapper.device.blocks
for block in self.wrapper.device.blocks:
if block.type == "device":
self.device_block = block
if hasattr(block, "targetTemp"):
self.block = block
_LOGGER.debug("Entity %s attached to block", self.name)
self.async_write_ha_state()
return

View File

@@ -612,10 +612,24 @@ class SimpliSafe:
data={**self.entry.data, CONF_TOKEN: token},
)
@callback
def async_handle_refresh_token(token: str) -> None:
"""Handle a new refresh token."""
async_save_refresh_token(token)
if TYPE_CHECKING:
assert self._api.websocket
if self._api.websocket.connected:
# If a websocket connection is open, reconnect it to use the
# new access token:
asyncio.create_task(self._api.websocket.async_reconnect())
self.entry.async_on_unload(
self._api.add_refresh_token_callback(async_save_refresh_token)
self._api.add_refresh_token_callback(async_handle_refresh_token)
)
# Save the refresh token we got on entry setup:
async_save_refresh_token(self._api.refresh_token)
async def async_update(self) -> None:

View File

@@ -5,7 +5,7 @@
"documentation": "https://www.home-assistant.io/integrations/smappee",
"dependencies": ["http"],
"requirements": [
"pysmappee==0.2.27"
"pysmappee==0.2.29"
],
"codeowners": [
"@bsmappee"

View File

@@ -3,7 +3,7 @@
"name": "Tailscale",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tailscale",
"requirements": ["tailscale==0.1.4"],
"requirements": ["tailscale==0.1.5"],
"codeowners": ["@frenck"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"

View File

@@ -641,9 +641,13 @@ class LightTemplate(TemplateEntity, LightEntity):
@callback
def _update_color(self, render):
"""Update the hs_color from the template."""
if render is None:
self._color = None
return
h_str = s_str = None
if isinstance(render, str):
if render in (None, "None", ""):
if render in ("None", ""):
self._color = None
return
h_str, s_str = map(

View File

@@ -2,7 +2,7 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
"requirements": ["pyTibber==0.21.0"],
"requirements": ["pyTibber==0.21.1"],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver",
"config_flow": true,

View File

@@ -2,7 +2,7 @@
"domain": "totalconnect",
"name": "Total Connect",
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
"requirements": ["total_connect_client==2021.11.4"],
"requirements": ["total_connect_client==2021.12"],
"dependencies": [],
"codeowners": ["@austinmroczek"],
"config_flow": true,

View File

@@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from . import ValloxDataUpdateCoordinator
from .const import (
@@ -107,7 +108,7 @@ class ValloxFilterRemainingSensor(ValloxSensor):
days_remaining_delta = timedelta(days=days_remaining)
now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0)
return now + days_remaining_delta
return (now + days_remaining_delta).astimezone(dt_util.UTC)
class ValloxCellStateSensor(ValloxSensor):

View File

@@ -3,7 +3,7 @@
"name": "Xiaomi Miio",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.1"],
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.2"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
"zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling"

View File

@@ -206,11 +206,11 @@ class Thermostat(ZhaEntity, ClimateEntity):
unoccupied_cooling_setpoint = self._thrm.unoccupied_cooling_setpoint
if unoccupied_cooling_setpoint is not None:
data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_cooling_setpoint
data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_cooling_setpoint
unoccupied_heating_setpoint = self._thrm.unoccupied_heating_setpoint
if unoccupied_heating_setpoint is not None:
data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_heating_setpoint
data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_heating_setpoint
return data
@property

View File

@@ -234,6 +234,7 @@ class GroupProbe:
unsub()
self._unsubs.remove(unsub)
@callback
def _reprobe_group(self, group_id: int) -> None:
"""Reprobe a group for entities after its members change."""
zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]

View File

@@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "1"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@@ -1052,6 +1052,7 @@ class Script:
if self._change_listener_job:
self._hass.async_run_hass_job(self._change_listener_job)
@callback
def _chain_change_listener(self, sub_script: Script) -> None:
if sub_script.is_running:
self.last_action = sub_script.last_action

View File

@@ -16,7 +16,7 @@ ciso8601==2.2.0
cryptography==35.0.0
emoji==1.5.0
hass-nabucasa==0.50.0
home-assistant-frontend==20211212.0
home-assistant-frontend==20211215.0
httpx==0.21.0
ifaddr==0.1.7
jinja2==3.0.3

View File

@@ -186,7 +186,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.3
aiohue==3.0.6
# homeassistant.components.imap
aioimaplib==0.9.0
@@ -440,7 +440,7 @@ brother==1.1.0
brottsplatskartan==0.0.1
# homeassistant.components.brunt
brunt==1.0.0
brunt==1.0.2
# homeassistant.components.bsblan
bsblan==0.4.0
@@ -573,7 +573,7 @@ dweepy==0.3.0
dynalite_devices==0.1.46
# homeassistant.components.ebusd
ebusdpy==0.0.16
ebusdpy==0.0.17
# homeassistant.components.ecoal_boiler
ecoaliface==0.4.0
@@ -600,7 +600,7 @@ enocean==0.50
enturclient==0.2.2
# homeassistant.components.environment_canada
env_canada==0.5.18
env_canada==0.5.20
# homeassistant.components.envirophat
# envirophat==0.0.6
@@ -658,7 +658,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.26.7
flux_led==0.26.15
# homeassistant.components.homekit
fnvhash==0.1.0
@@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0
google-cloud-texttospeech==0.4.0
# homeassistant.components.nest
google-nest-sdm==0.4.6
google-nest-sdm==0.4.9
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@@ -819,7 +819,7 @@ hole==0.7.0
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211212.0
home-assistant-frontend==20211215.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -968,6 +968,9 @@ london-tube-status==0.2
# homeassistant.components.luftdaten
luftdaten==0.7.1
# homeassistant.components.lupusec
lupupy==0.0.24
# homeassistant.components.lw12wifi
lw12==0.9.2
@@ -1324,7 +1327,7 @@ pyRFXtrx==0.27.0
# pySwitchmate==0.4.6
# homeassistant.components.tibber
pyTibber==0.21.0
pyTibber==0.21.1
# homeassistant.components.dlink
pyW215==0.7.0
@@ -1393,7 +1396,7 @@ pycfdns==1.2.2
pychannels==1.0.0
# homeassistant.components.cast
pychromecast==10.1.1
pychromecast==10.2.1
# homeassistant.components.pocketcasts
pycketcasts==1.0.0
@@ -1628,7 +1631,7 @@ pymazda==0.2.2
pymediaroom==0.6.4.1
# homeassistant.components.melcloud
pymelcloud==2.5.5
pymelcloud==2.5.6
# homeassistant.components.meteoclimatic
pymeteoclimatic==0.0.6
@@ -1802,7 +1805,7 @@ pyskyqhub==0.1.3
pysma==0.6.9
# homeassistant.components.smappee
pysmappee==0.2.27
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartapp==0.3.3
@@ -1901,7 +1904,7 @@ python-kasa==0.4.0
# python-lirc==1.2.3
# homeassistant.components.xiaomi_miio
python-miio==0.5.9.1
python-miio==0.5.9.2
# homeassistant.components.mpd
python-mpd2==3.0.4
@@ -2269,7 +2272,7 @@ systembridge==2.2.3
tahoma-api==0.0.16
# homeassistant.components.tailscale
tailscale==0.1.4
tailscale==0.1.5
# homeassistant.components.tank_utility
tank_utility==1.4.0
@@ -2326,7 +2329,7 @@ tololib==0.1.0b3
toonapi==0.2.1
# homeassistant.components.totalconnect
total_connect_client==2021.11.4
total_connect_client==2021.12
# homeassistant.components.tplink_lte
tp-connected==0.0.4

View File

@@ -131,7 +131,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.3
aiohue==3.0.6
# homeassistant.components.apache_kafka
aiokafka==0.6.0
@@ -281,7 +281,7 @@ broadlink==0.18.0
brother==1.1.0
# homeassistant.components.brunt
brunt==1.0.0
brunt==1.0.2
# homeassistant.components.bsblan
bsblan==0.4.0
@@ -375,7 +375,7 @@ emulated_roku==0.2.1
enocean==0.50
# homeassistant.components.environment_canada
env_canada==0.5.18
env_canada==0.5.20
# homeassistant.components.enphase_envoy
envoy_reader==0.20.1
@@ -399,7 +399,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.26.7
flux_led==0.26.15
# homeassistant.components.homekit
fnvhash==0.1.0
@@ -461,7 +461,7 @@ google-api-python-client==1.6.4
google-cloud-pubsub==2.1.0
# homeassistant.components.nest
google-nest-sdm==0.4.6
google-nest-sdm==0.4.9
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@@ -515,7 +515,7 @@ hole==0.7.0
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211212.0
home-assistant-frontend==20211215.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -808,7 +808,7 @@ pyMetno==0.9.0
pyRFXtrx==0.27.0
# homeassistant.components.tibber
pyTibber==0.21.0
pyTibber==0.21.1
# homeassistant.components.nextbus
py_nextbusnext==0.1.5
@@ -850,7 +850,7 @@ pybotvac==0.0.22
pycfdns==1.2.2
# homeassistant.components.cast
pychromecast==10.1.1
pychromecast==10.2.1
# homeassistant.components.climacell
pyclimacell==0.18.2
@@ -995,7 +995,7 @@ pymata-express==1.19
pymazda==0.2.2
# homeassistant.components.melcloud
pymelcloud==2.5.5
pymelcloud==2.5.6
# homeassistant.components.meteoclimatic
pymeteoclimatic==0.0.6
@@ -1109,7 +1109,7 @@ pysignalclirestapi==0.3.4
pysma==0.6.9
# homeassistant.components.smappee
pysmappee==0.2.27
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartapp==0.3.3
@@ -1145,7 +1145,7 @@ python-juicenet==1.0.2
python-kasa==0.4.0
# homeassistant.components.xiaomi_miio
python-miio==0.5.9.1
python-miio==0.5.9.2
# homeassistant.components.nest
python-nest==4.1.0
@@ -1352,7 +1352,7 @@ surepy==0.7.2
systembridge==2.2.3
# homeassistant.components.tailscale
tailscale==0.1.4
tailscale==0.1.5
# homeassistant.components.tellduslive
tellduslive==0.10.11
@@ -1370,7 +1370,7 @@ tololib==0.1.0b3
toonapi==0.2.1
# homeassistant.components.totalconnect
total_connect_client==2021.11.4
total_connect_client==2021.12
# homeassistant.components.transmission
transmissionrpc==0.11

View File

@@ -754,7 +754,7 @@ async def test_supported_features(
assert state.attributes.get("supported_features") == supported_features
async def test_entity_play_media(hass: HomeAssistant):
async def test_entity_play_media(hass: HomeAssistant, quick_play_mock):
"""Test playing media."""
entity_id = "media_player.speaker"
reg = er.async_get(hass)
@@ -776,8 +776,28 @@ async def test_entity_play_media(hass: HomeAssistant):
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
# Play_media
await common.async_play_media(hass, "audio", "best.mp3", entity_id)
chromecast.media_controller.play_media.assert_called_once_with("best.mp3", "audio")
await hass.services.async_call(
media_player.DOMAIN,
media_player.SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: entity_id,
media_player.ATTR_MEDIA_CONTENT_TYPE: "audio",
media_player.ATTR_MEDIA_CONTENT_ID: "best.mp3",
media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}},
},
blocking=True,
)
chromecast.media_controller.play_media.assert_not_called()
quick_play_mock.assert_called_once_with(
chromecast,
"homeassistant_media",
{
"media_id": "best.mp3",
"media_type": "audio",
"metadata": {"metadatatype": 3},
},
)
async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock):
@@ -865,7 +885,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock):
assert "App unknown not supported" in caplog.text
async def test_entity_play_media_sign_URL(hass: HomeAssistant):
async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock):
"""Test playing media."""
entity_id = "media_player.speaker"
@@ -886,8 +906,10 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant):
# Play_media
await common.async_play_media(hass, "audio", "/best.mp3", entity_id)
chromecast.media_controller.play_media.assert_called_once_with(ANY, "audio")
assert chromecast.media_controller.play_media.call_args[0][0].startswith(
quick_play_mock.assert_called_once_with(
chromecast, "homeassistant_media", {"media_id": ANY, "media_type": "audio"}
)
assert quick_play_mock.call_args[0][2]["media_id"].startswith(
"http://example.com:8123/best.mp3?authSig="
)
@@ -1231,7 +1253,7 @@ async def test_group_media_states(hass, mz_mock):
assert state.state == "playing"
async def test_group_media_control(hass, mz_mock):
async def test_group_media_control(hass, mz_mock, quick_play_mock):
"""Test media controls are handled by group if entity has no state."""
entity_id = "media_player.speaker"
reg = er.async_get(hass)
@@ -1286,7 +1308,12 @@ async def test_group_media_control(hass, mz_mock):
# Verify play_media is not forwarded
await common.async_play_media(hass, "music", "best.mp3", entity_id)
assert not grp_media.play_media.called
assert chromecast.media_controller.play_media.called
assert not chromecast.media_controller.play_media.called
quick_play_mock.assert_called_once_with(
chromecast,
"homeassistant_media",
{"media_id": "best.mp3", "media_type": "music"},
)
async def test_failed_cast_on_idle(hass, caplog):

View File

@@ -121,6 +121,17 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000
# test again with sending flash/alert
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": test_light_id, "flash": "long"},
blocking=True,
)
assert len(mock_bridge_v2.mock_requests) == 3
assert mock_bridge_v2.mock_requests[2]["json"]["on"]["on"] is True
assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe"
async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data):
"""Test calling the turn off service on a light."""
@@ -295,7 +306,12 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": test_light_id, "brightness_pct": 100, "xy_color": (0.123, 0.123)},
{
"entity_id": test_light_id,
"brightness_pct": 100,
"xy_color": (0.123, 0.123),
"transition": 6,
},
blocking=True,
)
@@ -308,6 +324,9 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
)
assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["x"] == 0.123
assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 0.123
assert (
mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000
)
# Now generate update events by emitting the json we've sent as incoming events
for index in range(0, 3):
@@ -346,3 +365,24 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
test_light = hass.states.get(test_light_id)
assert test_light is not None
assert test_light.state == "off"
# Test calling the turn off service on a grouped light with transition
mock_bridge_v2.mock_requests.clear()
test_light_id = "light.test_zone"
await hass.services.async_call(
"light",
"turn_off",
{
"entity_id": test_light_id,
"transition": 6,
},
blocking=True,
)
# PUT request should have been sent to ALL group lights with correct params
assert len(mock_bridge_v2.mock_requests) == 3
for index in range(0, 3):
assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is False
assert (
mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000
)

View File

@@ -83,6 +83,60 @@ async def test_routing_setup(hass: HomeAssistant) -> None:
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_routing_setup_advanced(hass: HomeAssistant) -> None:
"""Test routing setup with advanced options."""
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = []
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_USER,
"show_advanced_options": True,
},
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_FORM
assert result2["step_id"] == "routing"
assert not result2["errors"]
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == CONF_KNX_ROUTING.capitalize()
assert result3["data"] == {
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
}
@@ -144,7 +198,11 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None:
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = [gateway]
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
DOMAIN,
context={
"source": config_entries.SOURCE_USER,
"show_advanced_options": True,
},
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
@@ -563,7 +621,6 @@ async def test_tunneling_options_flow(
CONF_HOST: "192.168.1.1",
CONF_PORT: 3675,
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
)
@@ -581,7 +638,6 @@ async def test_tunneling_options_flow(
CONF_HOST: "192.168.1.1",
CONF_PORT: 3675,
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
}
@@ -611,6 +667,7 @@ async def test_advanced_options(
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
)
@@ -626,4 +683,5 @@ async def test_advanced_options(
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
}

View File

@@ -0,0 +1,78 @@
"""Test KNX init."""
import pytest
from xknx import XKNX
from xknx.io import ConnectionConfig, ConnectionType
from homeassistant.components.knx.const import (
CONF_KNX_AUTOMATIC,
CONF_KNX_CONNECTION_TYPE,
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_ROUTING,
CONF_KNX_TUNNELING,
DOMAIN as KNX_DOMAIN,
)
from homeassistant.components.knx.schema import ConnectionSchema
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from .conftest import KNXTestKit
from tests.common import MockConfigEntry
@pytest.mark.parametrize(
"config_entry_data,connection_config",
[
(
{
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
},
ConnectionConfig(),
),
(
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.1",
},
ConnectionConfig(
connection_type=ConnectionType.ROUTING, local_ip="192.168.1.1"
),
),
(
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
CONF_HOST: "192.168.0.2",
CONF_PORT: 3675,
ConnectionSchema.CONF_KNX_ROUTE_BACK: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
ConnectionConfig(
connection_type=ConnectionType.TUNNELING,
route_back=False,
gateway_ip="192.168.0.2",
gateway_port=3675,
local_ip="192.168.1.112",
auto_reconnect=True,
),
),
],
)
async def test_init_connection_handling(
hass: HomeAssistant, knx: KNXTestKit, config_entry_data, connection_config
):
"""Test correctly generating connection config."""
config_entry = MockConfigEntry(
title="KNX",
domain=KNX_DOMAIN,
data=config_entry_data,
)
knx.mock_config_entry = config_entry
await knx.setup_integration({})
assert hass.data.get(KNX_DOMAIN) is not None
assert (
hass.data[KNX_DOMAIN].connection_config().__dict__ == connection_config.__dict__
)

View File

@@ -39,13 +39,12 @@ async def async_setup_devices(hass, device_type, traits={}):
return await async_setup_sdm_platform(hass, PLATFORM, devices=devices)
def create_device_traits(event_trait):
def create_device_traits(event_traits=[]):
"""Create fake traits for a device."""
return {
result = {
"sdm.devices.traits.Info": {
"customName": "Front",
},
event_trait: {},
"sdm.devices.traits.CameraLiveStream": {
"maxVideoResolution": {
"width": 640,
@@ -55,6 +54,8 @@ def create_device_traits(event_trait):
"audioCodecs": ["AAC"],
},
}
result.update({t: {} for t in event_traits})
return result
def create_event(event_type, device_id=DEVICE_ID, timestamp=None):
@@ -91,7 +92,7 @@ async def test_doorbell_chime_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.DoorbellChime"),
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
registry = er.async_get(hass)
@@ -129,7 +130,7 @@ async def test_camera_motion_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.CAMERA",
create_device_traits("sdm.devices.traits.CameraMotion"),
create_device_traits(["sdm.devices.traits.CameraMotion"]),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
@@ -157,7 +158,7 @@ async def test_camera_sound_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.CAMERA",
create_device_traits("sdm.devices.traits.CameraSound"),
create_device_traits(["sdm.devices.traits.CameraSound"]),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
@@ -185,7 +186,7 @@ async def test_camera_person_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.CameraEventImage"),
create_device_traits(["sdm.devices.traits.CameraPerson"]),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
@@ -213,7 +214,9 @@ async def test_camera_multiple_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.CameraEventImage"),
create_device_traits(
["sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraPerson"]
),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
@@ -256,7 +259,7 @@ async def test_unknown_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.DoorbellChime"),
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
await subscriber.async_receive_event(create_event("some-event-id"))
await hass.async_block_till_done()
@@ -270,7 +273,7 @@ async def test_unknown_device_id(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.DoorbellChime"),
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
await subscriber.async_receive_event(
create_event("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id")
@@ -286,7 +289,7 @@ async def test_event_message_without_device_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.DoorbellChime"),
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
timestamp = utcnow()
event = EventMessage(
@@ -308,14 +311,12 @@ async def test_doorbell_event_thread(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
traits={
"sdm.devices.traits.Info": {
"customName": "Front",
},
"sdm.devices.traits.CameraLiveStream": {},
"sdm.devices.traits.CameraClipPreview": {},
"sdm.devices.traits.CameraPerson": {},
},
create_device_traits(
[
"sdm.devices.traits.CameraClipPreview",
"sdm.devices.traits.CameraPerson",
]
),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
@@ -351,7 +352,7 @@ async def test_doorbell_event_thread(hass):
)
await subscriber.async_receive_event(EventMessage(message_data_1, auth=None))
# Publish message #1 that sends a no-op update to end the event thread
# Publish message #2 that sends a no-op update to end the event thread
timestamp2 = timestamp1 + datetime.timedelta(seconds=1)
message_data_2 = event_message_data.copy()
message_data_2.update(
@@ -371,3 +372,77 @@ async def test_doorbell_event_thread(hass):
"timestamp": timestamp1.replace(microsecond=0),
"nest_event_id": EVENT_SESSION_ID,
}
async def test_doorbell_event_session_update(hass):
"""Test a pubsub message with updates to an existing session."""
events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(
[
"sdm.devices.traits.CameraClipPreview",
"sdm.devices.traits.CameraPerson",
"sdm.devices.traits.CameraMotion",
]
),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
assert entry is not None
# Message #1 has a motion event
timestamp1 = utcnow()
await subscriber.async_receive_event(
create_events(
{
"sdm.devices.events.CameraMotion.Motion": {
"eventSessionId": EVENT_SESSION_ID,
"eventId": "n:1",
},
"sdm.devices.events.CameraClipPreview.ClipPreview": {
"eventSessionId": EVENT_SESSION_ID,
"previewUrl": "image-url-1",
},
},
timestamp=timestamp1,
)
)
# Message #2 has an extra person event
timestamp2 = utcnow()
await subscriber.async_receive_event(
create_events(
{
"sdm.devices.events.CameraMotion.Motion": {
"eventSessionId": EVENT_SESSION_ID,
"eventId": "n:1",
},
"sdm.devices.events.CameraPerson.Person": {
"eventSessionId": EVENT_SESSION_ID,
"eventId": "n:2",
},
"sdm.devices.events.CameraClipPreview.ClipPreview": {
"eventSessionId": EVENT_SESSION_ID,
"previewUrl": "image-url-1",
},
},
timestamp=timestamp2,
)
)
await hass.async_block_till_done()
assert len(events) == 2
assert events[0].data == {
"device_id": entry.device_id,
"type": "camera_motion",
"timestamp": timestamp1.replace(microsecond=0),
"nest_event_id": EVENT_SESSION_ID,
}
assert events[1].data == {
"device_id": entry.device_id,
"type": "camera_person",
"timestamp": timestamp2.replace(microsecond=0),
"nest_event_id": EVENT_SESSION_ID,
}

View File

@@ -16,6 +16,7 @@ from homeassistant.components import media_source
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.media_source import const
from homeassistant.components.media_source.error import Unresolvable
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.template import DATE_STR_FORMAT
import homeassistant.util.dt as dt_util
@@ -164,6 +165,37 @@ async def test_supported_device(hass, auth):
assert len(browse.children) == 0
async def test_integration_unloaded(hass, auth):
"""Test the media player loads, but has no devices, when config unloaded."""
await async_setup_devices(
hass,
auth,
CAMERA_DEVICE_TYPE,
CAMERA_TRAITS,
)
browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}")
assert browse.domain == DOMAIN
assert browse.identifier == ""
assert browse.title == "Nest"
assert len(browse.children) == 1
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
entry = entries[0]
assert entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(entry.entry_id)
assert entry.state == ConfigEntryState.NOT_LOADED
# No devices returned
browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}")
assert browse.domain == DOMAIN
assert browse.identifier == ""
assert browse.title == "Nest"
assert len(browse.children) == 0
async def test_camera_event(hass, auth, hass_client):
"""Test a media source and image created for an event."""
event_timestamp = dt_util.now()

View File

@@ -40,6 +40,7 @@ BASIC_RESULTS = {
{"minutes": "1", "epochTime": "1553807371000"},
{"minutes": "2", "epochTime": "1553807372000"},
{"minutes": "3", "epochTime": "1553807373000"},
{"minutes": "10", "epochTime": "1553807380000"},
],
},
}
@@ -128,7 +129,7 @@ async def test_verify_valid_state(
assert state.attributes["route"] == VALID_ROUTE_TITLE
assert state.attributes["stop"] == VALID_STOP_TITLE
assert state.attributes["direction"] == "Outbound"
assert state.attributes["upcoming"] == "1, 2, 3"
assert state.attributes["upcoming"] == "1, 2, 3, 10"
async def test_message_dict(

View File

@@ -1,6 +1,8 @@
"""Tests for 1-Wire config flow."""
import logging
from unittest.mock import MagicMock
from pyownet import protocol
import pytest
from homeassistant.components.onewire.const import DOMAIN
@@ -19,6 +21,20 @@ async def test_owserver_connect_failure(hass: HomeAssistant, config_entry: Confi
assert not hass.data.get(DOMAIN)
async def test_owserver_listing_failure(
hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock
):
"""Test listing failure raises ConfigEntryNotReady."""
owproxy.return_value.dir.side_effect = protocol.OwnetError()
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert config_entry.state is ConfigEntryState.SETUP_RETRY
assert not hass.data.get(DOMAIN)
@pytest.mark.usefixtures("owproxy")
async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Test being able to unload an entry."""