mirror of
https://github.com/home-assistant/core.git
synced 2025-07-30 16:57:19 +00:00
Merge pull request #61902 from home-assistant/rc
This commit is contained in:
commit
6d8d472f0f
15
.github/workflows/builder.yml
vendored
15
.github/workflows/builder.yml
vendored
@ -131,7 +131,7 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2021.11.4
|
uses: home-assistant/builder@2021.12.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@ -170,6 +170,17 @@ jobs:
|
|||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v2.4.0
|
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
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1.10.0
|
uses: docker/login-action@v1.10.0
|
||||||
with:
|
with:
|
||||||
@ -184,7 +195,7 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2021.11.4
|
uses: home-assistant/builder@2021.12.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Brunt Blind Engine",
|
"name": "Brunt Blind Engine",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/brunt",
|
"documentation": "https://www.home-assistant.io/integrations/brunt",
|
||||||
"requirements": ["brunt==1.0.0"],
|
"requirements": ["brunt==1.0.1"],
|
||||||
"codeowners": ["@eavanvalkenburg"],
|
"codeowners": ["@eavanvalkenburg"],
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Google Cast",
|
"name": "Google Cast",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||||
"requirements": ["pychromecast==10.1.1"],
|
"requirements": ["pychromecast==10.2.1"],
|
||||||
"after_dependencies": [
|
"after_dependencies": [
|
||||||
"cloud",
|
"cloud",
|
||||||
"http",
|
"http",
|
||||||
|
@ -47,7 +47,6 @@ from homeassistant.components.plex.const import PLEX_URI_SCHEME
|
|||||||
from homeassistant.components.plex.services import lookup_plex_media
|
from homeassistant.components.plex.services import lookup_plex_media
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
|
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
|
||||||
CAST_APP_ID_HOMEASSISTANT_MEDIA,
|
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
STATE_IDLE,
|
STATE_IDLE,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
@ -230,7 +229,6 @@ class CastDevice(MediaPlayerEntity):
|
|||||||
self._cast_info.cast_info,
|
self._cast_info.cast_info,
|
||||||
ChromeCastZeroconf.get_zeroconf(),
|
ChromeCastZeroconf.get_zeroconf(),
|
||||||
)
|
)
|
||||||
chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA
|
|
||||||
self._chromecast = chromecast
|
self._chromecast = chromecast
|
||||||
|
|
||||||
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
|
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
|
||||||
@ -527,9 +525,8 @@ class CastDevice(MediaPlayerEntity):
|
|||||||
self._chromecast.register_handler(controller)
|
self._chromecast.register_handler(controller)
|
||||||
controller.play_media(media)
|
controller.play_media(media)
|
||||||
else:
|
else:
|
||||||
self._chromecast.media_controller.play_media(
|
app_data = {"media_id": media_id, "media_type": media_type, **extra}
|
||||||
media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {})
|
quick_play(self._chromecast, "homeassistant_media", app_data)
|
||||||
)
|
|
||||||
|
|
||||||
def _media_status(self):
|
def _media_status(self):
|
||||||
"""
|
"""
|
||||||
@ -820,7 +817,6 @@ class DynamicCastGroup:
|
|||||||
self._cast_info.cast_info,
|
self._cast_info.cast_info,
|
||||||
ChromeCastZeroconf.get_zeroconf(),
|
ChromeCastZeroconf.get_zeroconf(),
|
||||||
)
|
)
|
||||||
chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA
|
|
||||||
self._chromecast = chromecast
|
self._chromecast = chromecast
|
||||||
|
|
||||||
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
|
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "environment_canada",
|
"domain": "environment_canada",
|
||||||
"name": "Environment Canada",
|
"name": "Environment Canada",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/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"],
|
"codeowners": ["@gwww", "@michaeldavie"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Home Assistant Frontend",
|
"name": "Home Assistant Frontend",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"home-assistant-frontend==20211212.0"
|
"home-assistant-frontend==20211215.0"
|
||||||
],
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"api",
|
"api",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Philips Hue",
|
"name": "Philips Hue",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/hue",
|
"documentation": "https://www.home-assistant.io/integrations/hue",
|
||||||
"requirements": ["aiohue==3.0.3"],
|
"requirements": ["aiohue==3.0.5"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Royal Philips Electronics",
|
"manufacturer": "Royal Philips Electronics",
|
||||||
|
@ -7,6 +7,7 @@ from aiohue.v2.models.button import ButtonEvent
|
|||||||
from aiohue.v2.models.resource import ResourceTypes
|
from aiohue.v2.models.resource import ResourceTypes
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import persistent_notification
|
||||||
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||||
from homeassistant.components.homeassistant.triggers import event as event_trigger
|
from homeassistant.components.homeassistant.triggers import event as event_trigger
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -35,7 +36,7 @@ if TYPE_CHECKING:
|
|||||||
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_TYPE): str,
|
vol.Required(CONF_TYPE): str,
|
||||||
vol.Required(CONF_SUBTYPE): int,
|
vol.Required(CONF_SUBTYPE): vol.Union(int, str),
|
||||||
vol.Optional(CONF_UNIQUE_ID): 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(
|
async def async_validate_trigger_config(
|
||||||
bridge: "HueBridge",
|
bridge: "HueBridge",
|
||||||
device_entry: DeviceEntry,
|
device_entry: DeviceEntry,
|
||||||
@ -61,6 +89,7 @@ async def async_validate_trigger_config(
|
|||||||
):
|
):
|
||||||
"""Validate config."""
|
"""Validate config."""
|
||||||
config = TRIGGER_SCHEMA(config)
|
config = TRIGGER_SCHEMA(config)
|
||||||
|
check_invalid_device_trigger(bridge, config, device_entry)
|
||||||
return config
|
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(
|
return await event_trigger.async_attach_trigger(
|
||||||
hass, event_config, action, automation_info, platform_type="device"
|
hass, event_config, action, automation_info, platform_type="device"
|
||||||
)
|
)
|
||||||
|
@ -47,6 +47,20 @@ class HueBaseEntity(Entity):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, self.device.id)},
|
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
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@ -98,13 +112,12 @@ class HueBaseEntity(Entity):
|
|||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return entity availability."""
|
"""Return entity availability."""
|
||||||
if self.device is None:
|
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
|
return True
|
||||||
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
|
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
|
||||||
# the zigbee connectivity sensor itself should be always available
|
# the zigbee connectivity sensor itself should be always available
|
||||||
return True
|
return True
|
||||||
if self.device.product_data.manufacturer_name != "Signify Netherlands B.V.":
|
if self._ignore_availability:
|
||||||
# availability status for non-philips brand lights is unreliable
|
|
||||||
return True
|
return True
|
||||||
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
||||||
# all device-attached entities get availability from the zigbee connectivity
|
# all device-attached entities get availability from the zigbee connectivity
|
||||||
|
@ -6,16 +6,19 @@ from typing import Any
|
|||||||
from aiohue.v2 import HueBridgeV2
|
from aiohue.v2 import HueBridgeV2
|
||||||
from aiohue.v2.controllers.events import EventType
|
from aiohue.v2.controllers.events import EventType
|
||||||
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
|
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
|
||||||
|
from aiohue.v2.models.feature import AlertEffectType
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_COLOR_TEMP,
|
ATTR_COLOR_TEMP,
|
||||||
|
ATTR_FLASH,
|
||||||
ATTR_TRANSITION,
|
ATTR_TRANSITION,
|
||||||
ATTR_XY_COLOR,
|
ATTR_XY_COLOR,
|
||||||
COLOR_MODE_BRIGHTNESS,
|
COLOR_MODE_BRIGHTNESS,
|
||||||
COLOR_MODE_COLOR_TEMP,
|
COLOR_MODE_COLOR_TEMP,
|
||||||
COLOR_MODE_ONOFF,
|
COLOR_MODE_ONOFF,
|
||||||
COLOR_MODE_XY,
|
COLOR_MODE_XY,
|
||||||
|
SUPPORT_FLASH,
|
||||||
SUPPORT_TRANSITION,
|
SUPPORT_TRANSITION,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
@ -32,6 +35,7 @@ ALLOWED_ERRORS = [
|
|||||||
'device (groupedLight) is "soft off", command (on) may not have effect',
|
'device (groupedLight) is "soft off", command (on) may not have effect',
|
||||||
"device (light) has communication issues, 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',
|
'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.group = group
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.api: HueBridgeV2 = bridge.api
|
self.api: HueBridgeV2 = bridge.api
|
||||||
|
self._attr_supported_features |= SUPPORT_FLASH
|
||||||
self._attr_supported_features |= SUPPORT_TRANSITION
|
self._attr_supported_features |= SUPPORT_TRANSITION
|
||||||
|
|
||||||
# Entities for Hue groups are disabled by default
|
# Entities for Hue groups are disabled by default
|
||||||
@ -146,6 +151,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||||||
xy_color = kwargs.get(ATTR_XY_COLOR)
|
xy_color = kwargs.get(ATTR_XY_COLOR)
|
||||||
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
||||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
|
flash = kwargs.get(ATTR_FLASH)
|
||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
# Hue uses a range of [0, 100] to control brightness.
|
# Hue uses a range of [0, 100] to control brightness.
|
||||||
brightness = float((brightness / 255) * 100)
|
brightness = float((brightness / 255) * 100)
|
||||||
@ -160,6 +166,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||||||
and xy_color is None
|
and xy_color is None
|
||||||
and color_temp is None
|
and color_temp is None
|
||||||
and transition is None
|
and transition is None
|
||||||
|
and flash is None
|
||||||
):
|
):
|
||||||
await self.bridge.async_request_call(
|
await self.bridge.async_request_call(
|
||||||
self.controller.set_state,
|
self.controller.set_state,
|
||||||
@ -180,17 +187,37 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||||||
color_xy=xy_color if light.supports_color else None,
|
color_xy=xy_color if light.supports_color else None,
|
||||||
color_temp=color_temp if light.supports_color_temperature else None,
|
color_temp=color_temp if light.supports_color_temperature else None,
|
||||||
transition_time=transition,
|
transition_time=transition,
|
||||||
|
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||||
allowed_errors=ALLOWED_ERRORS,
|
allowed_errors=ALLOWED_ERRORS,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the light off."""
|
"""Turn the light off."""
|
||||||
|
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(
|
await self.bridge.async_request_call(
|
||||||
self.controller.set_state,
|
self.controller.set_state,
|
||||||
id=self.resource.id,
|
id=self.resource.id,
|
||||||
on=False,
|
on=False,
|
||||||
allowed_errors=ALLOWED_ERRORS,
|
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
|
@callback
|
||||||
def on_update(self) -> None:
|
def on_update(self) -> None:
|
||||||
|
@ -6,17 +6,20 @@ from typing import Any
|
|||||||
from aiohue import HueBridgeV2
|
from aiohue import HueBridgeV2
|
||||||
from aiohue.v2.controllers.events import EventType
|
from aiohue.v2.controllers.events import EventType
|
||||||
from aiohue.v2.controllers.lights import LightsController
|
from aiohue.v2.controllers.lights import LightsController
|
||||||
|
from aiohue.v2.models.feature import AlertEffectType
|
||||||
from aiohue.v2.models.light import Light
|
from aiohue.v2.models.light import Light
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_COLOR_TEMP,
|
ATTR_COLOR_TEMP,
|
||||||
|
ATTR_FLASH,
|
||||||
ATTR_TRANSITION,
|
ATTR_TRANSITION,
|
||||||
ATTR_XY_COLOR,
|
ATTR_XY_COLOR,
|
||||||
COLOR_MODE_BRIGHTNESS,
|
COLOR_MODE_BRIGHTNESS,
|
||||||
COLOR_MODE_COLOR_TEMP,
|
COLOR_MODE_COLOR_TEMP,
|
||||||
COLOR_MODE_ONOFF,
|
COLOR_MODE_ONOFF,
|
||||||
COLOR_MODE_XY,
|
COLOR_MODE_XY,
|
||||||
|
SUPPORT_FLASH,
|
||||||
SUPPORT_TRANSITION,
|
SUPPORT_TRANSITION,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
@ -31,6 +34,7 @@ from .entity import HueBaseEntity
|
|||||||
ALLOWED_ERRORS = [
|
ALLOWED_ERRORS = [
|
||||||
"device (light) has communication issues, 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',
|
'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:
|
) -> None:
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
super().__init__(bridge, controller, resource)
|
super().__init__(bridge, controller, resource)
|
||||||
|
self._attr_supported_features |= SUPPORT_FLASH
|
||||||
self.resource = resource
|
self.resource = resource
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self._supported_color_modes = set()
|
self._supported_color_modes = set()
|
||||||
@ -154,6 +159,7 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
xy_color = kwargs.get(ATTR_XY_COLOR)
|
xy_color = kwargs.get(ATTR_XY_COLOR)
|
||||||
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
||||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
|
flash = kwargs.get(ATTR_FLASH)
|
||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
# Hue uses a range of [0, 100] to control brightness.
|
# Hue uses a range of [0, 100] to control brightness.
|
||||||
brightness = float((brightness / 255) * 100)
|
brightness = float((brightness / 255) * 100)
|
||||||
@ -169,12 +175,14 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
color_xy=xy_color,
|
color_xy=xy_color,
|
||||||
color_temp=color_temp,
|
color_temp=color_temp,
|
||||||
transition_time=transition,
|
transition_time=transition,
|
||||||
|
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||||
allowed_errors=ALLOWED_ERRORS,
|
allowed_errors=ALLOWED_ERRORS,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the light off."""
|
"""Turn the light off."""
|
||||||
transition = kwargs.get(ATTR_TRANSITION)
|
transition = kwargs.get(ATTR_TRANSITION)
|
||||||
|
flash = kwargs.get(ATTR_FLASH)
|
||||||
if transition is not None:
|
if transition is not None:
|
||||||
# hue transition duration is in milliseconds
|
# hue transition duration is in milliseconds
|
||||||
transition = int(transition * 1000)
|
transition = int(transition * 1000)
|
||||||
@ -183,5 +191,6 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
id=self.resource.id,
|
id=self.resource.id,
|
||||||
on=False,
|
on=False,
|
||||||
transition_time=transition,
|
transition_time=transition,
|
||||||
|
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||||
allowed_errors=ALLOWED_ERRORS,
|
allowed_errors=ALLOWED_ERRORS,
|
||||||
)
|
)
|
||||||
|
@ -383,6 +383,7 @@ class KNXModule:
|
|||||||
if _conn_type == CONF_KNX_ROUTING:
|
if _conn_type == CONF_KNX_ROUTING:
|
||||||
return ConnectionConfig(
|
return ConnectionConfig(
|
||||||
connection_type=ConnectionType.ROUTING,
|
connection_type=ConnectionType.ROUTING,
|
||||||
|
local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP),
|
||||||
auto_reconnect=True,
|
auto_reconnect=True,
|
||||||
)
|
)
|
||||||
if _conn_type == CONF_KNX_TUNNELING:
|
if _conn_type == CONF_KNX_TUNNELING:
|
||||||
|
@ -137,9 +137,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
vol.Required(
|
vol.Required(
|
||||||
ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False
|
ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False
|
||||||
): vol.Coerce(bool),
|
): 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(
|
return self.async_show_form(
|
||||||
step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors
|
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: user_input[
|
||||||
CONF_KNX_INDIVIDUAL_ADDRESS
|
CONF_KNX_INDIVIDUAL_ADDRESS
|
||||||
],
|
],
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get(
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP
|
||||||
|
),
|
||||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
|
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -211,6 +216,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
): cv.port,
|
): cv.port,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.show_advanced_options:
|
||||||
|
fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="routing", data_schema=vol.Schema(fields), errors=errors
|
step_id="routing", data_schema=vol.Schema(fields), errors=errors
|
||||||
)
|
)
|
||||||
@ -306,7 +314,6 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
|||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
|
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
|
||||||
): cv.port,
|
): cv.port,
|
||||||
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
|
|
||||||
vol.Required(
|
vol.Required(
|
||||||
ConnectionSchema.CONF_KNX_ROUTE_BACK,
|
ConnectionSchema.CONF_KNX_ROUTE_BACK,
|
||||||
default=self.current_config.get(
|
default=self.current_config.get(
|
||||||
@ -381,6 +388,14 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.show_advanced_options:
|
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[
|
data_schema[
|
||||||
vol.Required(
|
vol.Required(
|
||||||
ConnectionSchema.CONF_KNX_STATE_UPDATER,
|
ConnectionSchema.CONF_KNX_STATE_UPDATER,
|
||||||
|
@ -28,7 +28,8 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"individual_address": "Individual address for the routing connection",
|
"individual_address": "Individual address for the routing connection",
|
||||||
"multicast_group": "The multicast group used for routing",
|
"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",
|
"individual_address": "Default individual address",
|
||||||
"multicast_group": "Multicast group used for routing and discovery",
|
"multicast_group": "Multicast group used for routing and discovery",
|
||||||
"multicast_port": "Multicast port 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",
|
"state_updater": "Globally enable reading states from the KNX Bus",
|
||||||
"rate_limit": "Maximum outgoing telegrams per second"
|
"rate_limit": "Maximum outgoing telegrams per second"
|
||||||
}
|
}
|
||||||
@ -56,8 +58,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"port": "[%key:common::config_flow::data::port%]",
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"route_back": "Route Back / NAT Mode",
|
"route_back": "Route Back / NAT Mode"
|
||||||
"local_ip": "Local IP (leave empty if unsure)"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,8 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"individual_address": "Individual address for the routing connection",
|
"individual_address": "Individual address for the routing connection",
|
||||||
"multicast_group": "The multicast group used for routing",
|
"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."
|
"description": "Please configure the routing options."
|
||||||
},
|
},
|
||||||
@ -48,6 +49,7 @@
|
|||||||
"individual_address": "Default individual address",
|
"individual_address": "Default individual address",
|
||||||
"multicast_group": "Multicast group used for routing and discovery",
|
"multicast_group": "Multicast group used for routing and discovery",
|
||||||
"multicast_port": "Multicast port 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",
|
"rate_limit": "Maximum outgoing telegrams per second",
|
||||||
"state_updater": "Globally enable reading states from the KNX Bus"
|
"state_updater": "Globally enable reading states from the KNX Bus"
|
||||||
}
|
}
|
||||||
@ -55,7 +57,6 @@
|
|||||||
"tunnel": {
|
"tunnel": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"local_ip": "Local IP (leave empty if unsure)",
|
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"route_back": "Route Back / NAT Mode"
|
"route_back": "Route Back / NAT Mode"
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"disabled": "Library has incompatible requirements.",
|
|
||||||
"domain": "lupusec",
|
"domain": "lupusec",
|
||||||
"name": "Lupus Electronics LUPUSEC",
|
"name": "Lupus Electronics LUPUSEC",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/lupusec",
|
"documentation": "https://www.home-assistant.io/integrations/lupusec",
|
||||||
"requirements": ["lupupy==0.0.21"],
|
"requirements": ["lupupy==0.0.24"],
|
||||||
"codeowners": ["@majuss"],
|
"codeowners": ["@majuss"],
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "MELCloud",
|
"name": "MELCloud",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/melcloud",
|
"documentation": "https://www.home-assistant.io/integrations/melcloud",
|
||||||
"requirements": ["pymelcloud==2.5.5"],
|
"requirements": ["pymelcloud==2.5.6"],
|
||||||
"codeowners": ["@vilppuvuorinen"],
|
"codeowners": ["@vilppuvuorinen"],
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["ffmpeg", "http", "media_source"],
|
"dependencies": ["ffmpeg", "http", "media_source"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/nest",
|
"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.8"],
|
||||||
"codeowners": ["@allenporter"],
|
"codeowners": ["@allenporter"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"dhcp": [
|
"dhcp": [
|
||||||
|
@ -63,6 +63,9 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
|||||||
|
|
||||||
async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]:
|
async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]:
|
||||||
"""Return a mapping of device id to eligible Nest event media devices."""
|
"""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]
|
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
|
||||||
device_manager = await subscriber.async_get_device_manager()
|
device_manager = await subscriber.async_get_device_manager()
|
||||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
@ -612,10 +612,24 @@ class SimpliSafe:
|
|||||||
data={**self.entry.data, CONF_TOKEN: token},
|
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.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_save_refresh_token(self._api.refresh_token)
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/smappee",
|
"documentation": "https://www.home-assistant.io/integrations/smappee",
|
||||||
"dependencies": ["http"],
|
"dependencies": ["http"],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"pysmappee==0.2.27"
|
"pysmappee==0.2.29"
|
||||||
],
|
],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@bsmappee"
|
"@bsmappee"
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Tailscale",
|
"name": "Tailscale",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/tailscale",
|
"documentation": "https://www.home-assistant.io/integrations/tailscale",
|
||||||
"requirements": ["tailscale==0.1.4"],
|
"requirements": ["tailscale==0.1.5"],
|
||||||
"codeowners": ["@frenck"],
|
"codeowners": ["@frenck"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "tibber",
|
"domain": "tibber",
|
||||||
"name": "Tibber",
|
"name": "Tibber",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||||
"requirements": ["pyTibber==0.21.0"],
|
"requirements": ["pyTibber==0.21.1"],
|
||||||
"codeowners": ["@danielhiversen"],
|
"codeowners": ["@danielhiversen"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "totalconnect",
|
"domain": "totalconnect",
|
||||||
"name": "Total Connect",
|
"name": "Total Connect",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
|
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
|
||||||
"requirements": ["total_connect_client==2021.11.4"],
|
"requirements": ["total_connect_client==2021.12"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@austinmroczek"],
|
"codeowners": ["@austinmroczek"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Xiaomi Miio",
|
"name": "Xiaomi Miio",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
|
"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"],
|
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
|
||||||
"zeroconf": ["_miio._udp.local."],
|
"zeroconf": ["_miio._udp.local."],
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
|
@ -206,11 +206,11 @@ class Thermostat(ZhaEntity, ClimateEntity):
|
|||||||
|
|
||||||
unoccupied_cooling_setpoint = self._thrm.unoccupied_cooling_setpoint
|
unoccupied_cooling_setpoint = self._thrm.unoccupied_cooling_setpoint
|
||||||
if unoccupied_cooling_setpoint is not None:
|
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
|
unoccupied_heating_setpoint = self._thrm.unoccupied_heating_setpoint
|
||||||
if unoccupied_heating_setpoint is not None:
|
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
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
|
|||||||
|
|
||||||
MAJOR_VERSION: Final = 2021
|
MAJOR_VERSION: Final = 2021
|
||||||
MINOR_VERSION: Final = 12
|
MINOR_VERSION: Final = 12
|
||||||
PATCH_VERSION: Final = "1"
|
PATCH_VERSION: Final = "2"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||||
|
@ -16,7 +16,7 @@ ciso8601==2.2.0
|
|||||||
cryptography==35.0.0
|
cryptography==35.0.0
|
||||||
emoji==1.5.0
|
emoji==1.5.0
|
||||||
hass-nabucasa==0.50.0
|
hass-nabucasa==0.50.0
|
||||||
home-assistant-frontend==20211212.0
|
home-assistant-frontend==20211215.0
|
||||||
httpx==0.21.0
|
httpx==0.21.0
|
||||||
ifaddr==0.1.7
|
ifaddr==0.1.7
|
||||||
jinja2==3.0.3
|
jinja2==3.0.3
|
||||||
|
@ -186,7 +186,7 @@ aiohomekit==0.6.4
|
|||||||
aiohttp_cors==0.7.0
|
aiohttp_cors==0.7.0
|
||||||
|
|
||||||
# homeassistant.components.hue
|
# homeassistant.components.hue
|
||||||
aiohue==3.0.3
|
aiohue==3.0.5
|
||||||
|
|
||||||
# homeassistant.components.imap
|
# homeassistant.components.imap
|
||||||
aioimaplib==0.9.0
|
aioimaplib==0.9.0
|
||||||
@ -440,7 +440,7 @@ brother==1.1.0
|
|||||||
brottsplatskartan==0.0.1
|
brottsplatskartan==0.0.1
|
||||||
|
|
||||||
# homeassistant.components.brunt
|
# homeassistant.components.brunt
|
||||||
brunt==1.0.0
|
brunt==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.bsblan
|
# homeassistant.components.bsblan
|
||||||
bsblan==0.4.0
|
bsblan==0.4.0
|
||||||
@ -600,7 +600,7 @@ enocean==0.50
|
|||||||
enturclient==0.2.2
|
enturclient==0.2.2
|
||||||
|
|
||||||
# homeassistant.components.environment_canada
|
# homeassistant.components.environment_canada
|
||||||
env_canada==0.5.18
|
env_canada==0.5.20
|
||||||
|
|
||||||
# homeassistant.components.envirophat
|
# homeassistant.components.envirophat
|
||||||
# envirophat==0.0.6
|
# envirophat==0.0.6
|
||||||
@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0
|
|||||||
google-cloud-texttospeech==0.4.0
|
google-cloud-texttospeech==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.nest
|
# homeassistant.components.nest
|
||||||
google-nest-sdm==0.4.6
|
google-nest-sdm==0.4.8
|
||||||
|
|
||||||
# homeassistant.components.google_travel_time
|
# homeassistant.components.google_travel_time
|
||||||
googlemaps==2.5.1
|
googlemaps==2.5.1
|
||||||
@ -819,7 +819,7 @@ hole==0.7.0
|
|||||||
holidays==0.11.3.1
|
holidays==0.11.3.1
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20211212.0
|
home-assistant-frontend==20211215.0
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.10
|
homeassistant-pyozw==0.1.10
|
||||||
@ -968,6 +968,9 @@ london-tube-status==0.2
|
|||||||
# homeassistant.components.luftdaten
|
# homeassistant.components.luftdaten
|
||||||
luftdaten==0.7.1
|
luftdaten==0.7.1
|
||||||
|
|
||||||
|
# homeassistant.components.lupusec
|
||||||
|
lupupy==0.0.24
|
||||||
|
|
||||||
# homeassistant.components.lw12wifi
|
# homeassistant.components.lw12wifi
|
||||||
lw12==0.9.2
|
lw12==0.9.2
|
||||||
|
|
||||||
@ -1324,7 +1327,7 @@ pyRFXtrx==0.27.0
|
|||||||
# pySwitchmate==0.4.6
|
# pySwitchmate==0.4.6
|
||||||
|
|
||||||
# homeassistant.components.tibber
|
# homeassistant.components.tibber
|
||||||
pyTibber==0.21.0
|
pyTibber==0.21.1
|
||||||
|
|
||||||
# homeassistant.components.dlink
|
# homeassistant.components.dlink
|
||||||
pyW215==0.7.0
|
pyW215==0.7.0
|
||||||
@ -1393,7 +1396,7 @@ pycfdns==1.2.2
|
|||||||
pychannels==1.0.0
|
pychannels==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.cast
|
# homeassistant.components.cast
|
||||||
pychromecast==10.1.1
|
pychromecast==10.2.1
|
||||||
|
|
||||||
# homeassistant.components.pocketcasts
|
# homeassistant.components.pocketcasts
|
||||||
pycketcasts==1.0.0
|
pycketcasts==1.0.0
|
||||||
@ -1628,7 +1631,7 @@ pymazda==0.2.2
|
|||||||
pymediaroom==0.6.4.1
|
pymediaroom==0.6.4.1
|
||||||
|
|
||||||
# homeassistant.components.melcloud
|
# homeassistant.components.melcloud
|
||||||
pymelcloud==2.5.5
|
pymelcloud==2.5.6
|
||||||
|
|
||||||
# homeassistant.components.meteoclimatic
|
# homeassistant.components.meteoclimatic
|
||||||
pymeteoclimatic==0.0.6
|
pymeteoclimatic==0.0.6
|
||||||
@ -1802,7 +1805,7 @@ pyskyqhub==0.1.3
|
|||||||
pysma==0.6.9
|
pysma==0.6.9
|
||||||
|
|
||||||
# homeassistant.components.smappee
|
# homeassistant.components.smappee
|
||||||
pysmappee==0.2.27
|
pysmappee==0.2.29
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartapp==0.3.3
|
pysmartapp==0.3.3
|
||||||
@ -1901,7 +1904,7 @@ python-kasa==0.4.0
|
|||||||
# python-lirc==1.2.3
|
# python-lirc==1.2.3
|
||||||
|
|
||||||
# homeassistant.components.xiaomi_miio
|
# homeassistant.components.xiaomi_miio
|
||||||
python-miio==0.5.9.1
|
python-miio==0.5.9.2
|
||||||
|
|
||||||
# homeassistant.components.mpd
|
# homeassistant.components.mpd
|
||||||
python-mpd2==3.0.4
|
python-mpd2==3.0.4
|
||||||
@ -2269,7 +2272,7 @@ systembridge==2.2.3
|
|||||||
tahoma-api==0.0.16
|
tahoma-api==0.0.16
|
||||||
|
|
||||||
# homeassistant.components.tailscale
|
# homeassistant.components.tailscale
|
||||||
tailscale==0.1.4
|
tailscale==0.1.5
|
||||||
|
|
||||||
# homeassistant.components.tank_utility
|
# homeassistant.components.tank_utility
|
||||||
tank_utility==1.4.0
|
tank_utility==1.4.0
|
||||||
@ -2326,7 +2329,7 @@ tololib==0.1.0b3
|
|||||||
toonapi==0.2.1
|
toonapi==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.totalconnect
|
# homeassistant.components.totalconnect
|
||||||
total_connect_client==2021.11.4
|
total_connect_client==2021.12
|
||||||
|
|
||||||
# homeassistant.components.tplink_lte
|
# homeassistant.components.tplink_lte
|
||||||
tp-connected==0.0.4
|
tp-connected==0.0.4
|
||||||
|
@ -131,7 +131,7 @@ aiohomekit==0.6.4
|
|||||||
aiohttp_cors==0.7.0
|
aiohttp_cors==0.7.0
|
||||||
|
|
||||||
# homeassistant.components.hue
|
# homeassistant.components.hue
|
||||||
aiohue==3.0.3
|
aiohue==3.0.5
|
||||||
|
|
||||||
# homeassistant.components.apache_kafka
|
# homeassistant.components.apache_kafka
|
||||||
aiokafka==0.6.0
|
aiokafka==0.6.0
|
||||||
@ -281,7 +281,7 @@ broadlink==0.18.0
|
|||||||
brother==1.1.0
|
brother==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.brunt
|
# homeassistant.components.brunt
|
||||||
brunt==1.0.0
|
brunt==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.bsblan
|
# homeassistant.components.bsblan
|
||||||
bsblan==0.4.0
|
bsblan==0.4.0
|
||||||
@ -375,7 +375,7 @@ emulated_roku==0.2.1
|
|||||||
enocean==0.50
|
enocean==0.50
|
||||||
|
|
||||||
# homeassistant.components.environment_canada
|
# homeassistant.components.environment_canada
|
||||||
env_canada==0.5.18
|
env_canada==0.5.20
|
||||||
|
|
||||||
# homeassistant.components.enphase_envoy
|
# homeassistant.components.enphase_envoy
|
||||||
envoy_reader==0.20.1
|
envoy_reader==0.20.1
|
||||||
@ -461,7 +461,7 @@ google-api-python-client==1.6.4
|
|||||||
google-cloud-pubsub==2.1.0
|
google-cloud-pubsub==2.1.0
|
||||||
|
|
||||||
# homeassistant.components.nest
|
# homeassistant.components.nest
|
||||||
google-nest-sdm==0.4.6
|
google-nest-sdm==0.4.8
|
||||||
|
|
||||||
# homeassistant.components.google_travel_time
|
# homeassistant.components.google_travel_time
|
||||||
googlemaps==2.5.1
|
googlemaps==2.5.1
|
||||||
@ -515,7 +515,7 @@ hole==0.7.0
|
|||||||
holidays==0.11.3.1
|
holidays==0.11.3.1
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20211212.0
|
home-assistant-frontend==20211215.0
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.10
|
homeassistant-pyozw==0.1.10
|
||||||
@ -808,7 +808,7 @@ pyMetno==0.9.0
|
|||||||
pyRFXtrx==0.27.0
|
pyRFXtrx==0.27.0
|
||||||
|
|
||||||
# homeassistant.components.tibber
|
# homeassistant.components.tibber
|
||||||
pyTibber==0.21.0
|
pyTibber==0.21.1
|
||||||
|
|
||||||
# homeassistant.components.nextbus
|
# homeassistant.components.nextbus
|
||||||
py_nextbusnext==0.1.5
|
py_nextbusnext==0.1.5
|
||||||
@ -850,7 +850,7 @@ pybotvac==0.0.22
|
|||||||
pycfdns==1.2.2
|
pycfdns==1.2.2
|
||||||
|
|
||||||
# homeassistant.components.cast
|
# homeassistant.components.cast
|
||||||
pychromecast==10.1.1
|
pychromecast==10.2.1
|
||||||
|
|
||||||
# homeassistant.components.climacell
|
# homeassistant.components.climacell
|
||||||
pyclimacell==0.18.2
|
pyclimacell==0.18.2
|
||||||
@ -995,7 +995,7 @@ pymata-express==1.19
|
|||||||
pymazda==0.2.2
|
pymazda==0.2.2
|
||||||
|
|
||||||
# homeassistant.components.melcloud
|
# homeassistant.components.melcloud
|
||||||
pymelcloud==2.5.5
|
pymelcloud==2.5.6
|
||||||
|
|
||||||
# homeassistant.components.meteoclimatic
|
# homeassistant.components.meteoclimatic
|
||||||
pymeteoclimatic==0.0.6
|
pymeteoclimatic==0.0.6
|
||||||
@ -1109,7 +1109,7 @@ pysignalclirestapi==0.3.4
|
|||||||
pysma==0.6.9
|
pysma==0.6.9
|
||||||
|
|
||||||
# homeassistant.components.smappee
|
# homeassistant.components.smappee
|
||||||
pysmappee==0.2.27
|
pysmappee==0.2.29
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartapp==0.3.3
|
pysmartapp==0.3.3
|
||||||
@ -1145,7 +1145,7 @@ python-juicenet==1.0.2
|
|||||||
python-kasa==0.4.0
|
python-kasa==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.xiaomi_miio
|
# homeassistant.components.xiaomi_miio
|
||||||
python-miio==0.5.9.1
|
python-miio==0.5.9.2
|
||||||
|
|
||||||
# homeassistant.components.nest
|
# homeassistant.components.nest
|
||||||
python-nest==4.1.0
|
python-nest==4.1.0
|
||||||
@ -1352,7 +1352,7 @@ surepy==0.7.2
|
|||||||
systembridge==2.2.3
|
systembridge==2.2.3
|
||||||
|
|
||||||
# homeassistant.components.tailscale
|
# homeassistant.components.tailscale
|
||||||
tailscale==0.1.4
|
tailscale==0.1.5
|
||||||
|
|
||||||
# homeassistant.components.tellduslive
|
# homeassistant.components.tellduslive
|
||||||
tellduslive==0.10.11
|
tellduslive==0.10.11
|
||||||
@ -1370,7 +1370,7 @@ tololib==0.1.0b3
|
|||||||
toonapi==0.2.1
|
toonapi==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.totalconnect
|
# homeassistant.components.totalconnect
|
||||||
total_connect_client==2021.11.4
|
total_connect_client==2021.12
|
||||||
|
|
||||||
# homeassistant.components.transmission
|
# homeassistant.components.transmission
|
||||||
transmissionrpc==0.11
|
transmissionrpc==0.11
|
||||||
|
@ -754,7 +754,7 @@ async def test_supported_features(
|
|||||||
assert state.attributes.get("supported_features") == 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."""
|
"""Test playing media."""
|
||||||
entity_id = "media_player.speaker"
|
entity_id = "media_player.speaker"
|
||||||
reg = er.async_get(hass)
|
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))
|
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||||
|
|
||||||
# Play_media
|
# Play_media
|
||||||
await common.async_play_media(hass, "audio", "best.mp3", entity_id)
|
await hass.services.async_call(
|
||||||
chromecast.media_controller.play_media.assert_called_once_with("best.mp3", "audio")
|
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):
|
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
|
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."""
|
"""Test playing media."""
|
||||||
entity_id = "media_player.speaker"
|
entity_id = "media_player.speaker"
|
||||||
|
|
||||||
@ -886,8 +906,10 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant):
|
|||||||
|
|
||||||
# Play_media
|
# Play_media
|
||||||
await common.async_play_media(hass, "audio", "/best.mp3", entity_id)
|
await common.async_play_media(hass, "audio", "/best.mp3", entity_id)
|
||||||
chromecast.media_controller.play_media.assert_called_once_with(ANY, "audio")
|
quick_play_mock.assert_called_once_with(
|
||||||
assert chromecast.media_controller.play_media.call_args[0][0].startswith(
|
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="
|
"http://example.com:8123/best.mp3?authSig="
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1231,7 +1253,7 @@ async def test_group_media_states(hass, mz_mock):
|
|||||||
assert state.state == "playing"
|
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."""
|
"""Test media controls are handled by group if entity has no state."""
|
||||||
entity_id = "media_player.speaker"
|
entity_id = "media_player.speaker"
|
||||||
reg = er.async_get(hass)
|
reg = er.async_get(hass)
|
||||||
@ -1286,7 +1308,12 @@ async def test_group_media_control(hass, mz_mock):
|
|||||||
# Verify play_media is not forwarded
|
# Verify play_media is not forwarded
|
||||||
await common.async_play_media(hass, "music", "best.mp3", entity_id)
|
await common.async_play_media(hass, "music", "best.mp3", entity_id)
|
||||||
assert not grp_media.play_media.called
|
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):
|
async def test_failed_cast_on_idle(hass, caplog):
|
||||||
|
@ -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"]["on"]["on"] is True
|
||||||
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000
|
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):
|
async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data):
|
||||||
"""Test calling the turn off service on a light."""
|
"""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(
|
await hass.services.async_call(
|
||||||
"light",
|
"light",
|
||||||
"turn_on",
|
"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,
|
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"]["x"] == 0.123
|
||||||
assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 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
|
# Now generate update events by emitting the json we've sent as incoming events
|
||||||
for index in range(0, 3):
|
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)
|
test_light = hass.states.get(test_light_id)
|
||||||
assert test_light is not None
|
assert test_light is not None
|
||||||
assert test_light.state == "off"
|
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
|
||||||
|
)
|
||||||
|
@ -83,6 +83,60 @@ async def test_routing_setup(hass: HomeAssistant) -> None:
|
|||||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
|
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
|
||||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||||
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
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",
|
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:
|
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
|
||||||
gateways.return_value = [gateway]
|
gateways.return_value = [gateway]
|
||||||
result = await hass.config_entries.flow.async_init(
|
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 result["type"] == RESULT_TYPE_FORM
|
||||||
assert not result["errors"]
|
assert not result["errors"]
|
||||||
@ -563,7 +621,6 @@ async def test_tunneling_options_flow(
|
|||||||
CONF_HOST: "192.168.1.1",
|
CONF_HOST: "192.168.1.1",
|
||||||
CONF_PORT: 3675,
|
CONF_PORT: 3675,
|
||||||
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
|
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_HOST: "192.168.1.1",
|
||||||
CONF_PORT: 3675,
|
CONF_PORT: 3675,
|
||||||
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
|
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_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||||
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
||||||
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
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_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||||
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
||||||
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||||
}
|
}
|
||||||
|
78
tests/components/knx/test_init.py
Normal file
78
tests/components/knx/test_init.py
Normal 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__
|
||||||
|
)
|
@ -39,13 +39,12 @@ async def async_setup_devices(hass, device_type, traits={}):
|
|||||||
return await async_setup_sdm_platform(hass, PLATFORM, devices=devices)
|
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."""
|
"""Create fake traits for a device."""
|
||||||
return {
|
result = {
|
||||||
"sdm.devices.traits.Info": {
|
"sdm.devices.traits.Info": {
|
||||||
"customName": "Front",
|
"customName": "Front",
|
||||||
},
|
},
|
||||||
event_trait: {},
|
|
||||||
"sdm.devices.traits.CameraLiveStream": {
|
"sdm.devices.traits.CameraLiveStream": {
|
||||||
"maxVideoResolution": {
|
"maxVideoResolution": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
@ -55,6 +54,8 @@ def create_device_traits(event_trait):
|
|||||||
"audioCodecs": ["AAC"],
|
"audioCodecs": ["AAC"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
result.update({t: {} for t in event_traits})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def create_event(event_type, device_id=DEVICE_ID, timestamp=None):
|
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(
|
subscriber = await async_setup_devices(
|
||||||
hass,
|
hass,
|
||||||
"sdm.devices.types.DOORBELL",
|
"sdm.devices.types.DOORBELL",
|
||||||
create_device_traits("sdm.devices.traits.DoorbellChime"),
|
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
registry = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
@ -129,7 +130,7 @@ async def test_camera_motion_event(hass):
|
|||||||
subscriber = await async_setup_devices(
|
subscriber = await async_setup_devices(
|
||||||
hass,
|
hass,
|
||||||
"sdm.devices.types.CAMERA",
|
"sdm.devices.types.CAMERA",
|
||||||
create_device_traits("sdm.devices.traits.CameraMotion"),
|
create_device_traits(["sdm.devices.traits.CameraMotion"]),
|
||||||
)
|
)
|
||||||
registry = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
entry = registry.async_get("camera.front")
|
entry = registry.async_get("camera.front")
|
||||||
@ -157,7 +158,7 @@ async def test_camera_sound_event(hass):
|
|||||||
subscriber = await async_setup_devices(
|
subscriber = await async_setup_devices(
|
||||||
hass,
|
hass,
|
||||||
"sdm.devices.types.CAMERA",
|
"sdm.devices.types.CAMERA",
|
||||||
create_device_traits("sdm.devices.traits.CameraSound"),
|
create_device_traits(["sdm.devices.traits.CameraSound"]),
|
||||||
)
|
)
|
||||||
registry = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
entry = registry.async_get("camera.front")
|
entry = registry.async_get("camera.front")
|
||||||
@ -185,7 +186,7 @@ async def test_camera_person_event(hass):
|
|||||||
subscriber = await async_setup_devices(
|
subscriber = await async_setup_devices(
|
||||||
hass,
|
hass,
|
||||||
"sdm.devices.types.DOORBELL",
|
"sdm.devices.types.DOORBELL",
|
||||||
create_device_traits("sdm.devices.traits.CameraEventImage"),
|
create_device_traits(["sdm.devices.traits.CameraPerson"]),
|
||||||
)
|
)
|
||||||
registry = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
entry = registry.async_get("camera.front")
|
entry = registry.async_get("camera.front")
|
||||||
@ -213,7 +214,9 @@ async def test_camera_multiple_event(hass):
|
|||||||
subscriber = await async_setup_devices(
|
subscriber = await async_setup_devices(
|
||||||
hass,
|
hass,
|
||||||
"sdm.devices.types.DOORBELL",
|
"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)
|
registry = er.async_get(hass)
|
||||||
entry = registry.async_get("camera.front")
|
entry = registry.async_get("camera.front")
|
||||||
@ -256,7 +259,7 @@ async def test_unknown_event(hass):
|
|||||||
subscriber = await async_setup_devices(
|
subscriber = await async_setup_devices(
|
||||||
hass,
|
hass,
|
||||||
"sdm.devices.types.DOORBELL",
|
"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 subscriber.async_receive_event(create_event("some-event-id"))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -270,7 +273,7 @@ async def test_unknown_device_id(hass):
|
|||||||
subscriber = await async_setup_devices(
|
subscriber = await async_setup_devices(
|
||||||
hass,
|
hass,
|
||||||
"sdm.devices.types.DOORBELL",
|
"sdm.devices.types.DOORBELL",
|
||||||
create_device_traits("sdm.devices.traits.DoorbellChime"),
|
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
|
||||||
)
|
)
|
||||||
await subscriber.async_receive_event(
|
await subscriber.async_receive_event(
|
||||||
create_event("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id")
|
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(
|
subscriber = await async_setup_devices(
|
||||||
hass,
|
hass,
|
||||||
"sdm.devices.types.DOORBELL",
|
"sdm.devices.types.DOORBELL",
|
||||||
create_device_traits("sdm.devices.traits.DoorbellChime"),
|
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
|
||||||
)
|
)
|
||||||
timestamp = utcnow()
|
timestamp = utcnow()
|
||||||
event = EventMessage(
|
event = EventMessage(
|
||||||
@ -308,14 +311,12 @@ async def test_doorbell_event_thread(hass):
|
|||||||
subscriber = await async_setup_devices(
|
subscriber = await async_setup_devices(
|
||||||
hass,
|
hass,
|
||||||
"sdm.devices.types.DOORBELL",
|
"sdm.devices.types.DOORBELL",
|
||||||
traits={
|
create_device_traits(
|
||||||
"sdm.devices.traits.Info": {
|
[
|
||||||
"customName": "Front",
|
"sdm.devices.traits.CameraClipPreview",
|
||||||
},
|
"sdm.devices.traits.CameraPerson",
|
||||||
"sdm.devices.traits.CameraLiveStream": {},
|
]
|
||||||
"sdm.devices.traits.CameraClipPreview": {},
|
),
|
||||||
"sdm.devices.traits.CameraPerson": {},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
registry = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
entry = registry.async_get("camera.front")
|
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))
|
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)
|
timestamp2 = timestamp1 + datetime.timedelta(seconds=1)
|
||||||
message_data_2 = event_message_data.copy()
|
message_data_2 = event_message_data.copy()
|
||||||
message_data_2.update(
|
message_data_2.update(
|
||||||
@ -371,3 +372,77 @@ async def test_doorbell_event_thread(hass):
|
|||||||
"timestamp": timestamp1.replace(microsecond=0),
|
"timestamp": timestamp1.replace(microsecond=0),
|
||||||
"nest_event_id": EVENT_SESSION_ID,
|
"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,
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@ from homeassistant.components import media_source
|
|||||||
from homeassistant.components.media_player.errors import BrowseError
|
from homeassistant.components.media_player.errors import BrowseError
|
||||||
from homeassistant.components.media_source import const
|
from homeassistant.components.media_source import const
|
||||||
from homeassistant.components.media_source.error import Unresolvable
|
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 import device_registry as dr
|
||||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
@ -164,6 +165,37 @@ async def test_supported_device(hass, auth):
|
|||||||
assert len(browse.children) == 0
|
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):
|
async def test_camera_event(hass, auth, hass_client):
|
||||||
"""Test a media source and image created for an event."""
|
"""Test a media source and image created for an event."""
|
||||||
event_timestamp = dt_util.now()
|
event_timestamp = dt_util.now()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user