Compare commits

...

28 Commits

Author SHA1 Message Date
Paulus Schoutsen 6887474ddc Bumped version to 2021.3.0b4 2021-02-28 20:22:46 +00:00
J. Nick Koston e93868f85b Update HAP-python to 3.3.1 (#47180)
Fixes disconnect when setting a single char fails
https://github.com/ikalchev/HAP-python/compare/v3.3.0...v3.3.1
2021-02-28 20:22:29 +00:00
David F. Mulcahey db098d90dd Bump ZHA quirks to 0.0.54 (#47172) 2021-02-28 20:22:29 +00:00
Stefan Agner 552da0327e Bump builder to get generic-x86-64 nightly builds (#47164) 2021-02-28 20:22:28 +00:00
Erik Montnemery 104d5c510f Fix MQTT trigger where wanted payload may be parsed as an integer (#47162) 2021-02-28 20:22:27 +00:00
Paulus Schoutsen d9d979d50e Fix the updater schema (#47128) 2021-02-28 20:21:41 +00:00
Bram Kragten e65b2231ba Update frontend to 20210226.0 (#47123) 2021-02-28 20:21:41 +00:00
J. Nick Koston 2b0f6716b3 Provide a human readable exception for the percentage util (#47121) 2021-02-28 20:21:40 +00:00
J. Nick Koston dd4f8bf4b4 Handle lutron_caseta fan speed being none (#47120) 2021-02-28 20:21:39 +00:00
Shay Levy 505ca07c4e Fix Shelly RGBW (#47116) 2021-02-28 20:21:39 +00:00
Paulus Schoutsen 807bf15ff3 Use async_capture_events to avoid running in executor (#47111) 2021-02-28 20:21:38 +00:00
Paulus Schoutsen cdf7372fd8 Bumped version to 2021.3.0b3 2021-02-26 19:21:15 +00:00
Allen Porter 0969cc985b Bump google-nest-sdm to v0.2.12 to improve API call error messages (#47108) 2021-02-26 19:21:08 +00:00
Stefan Agner 5e2bafca56 Add new machine generic-x86-64 to build matrix (#47095)
The Intel NUC machine runs on most UEFI capable x86-64 machines today.
Lets start a new machine generic-x86-64 which will replace intel-nuc
over time.
2021-02-26 19:21:07 +00:00
Simone Chemelli 1d1be8ad1a Bump aioshelly to 0.6.1 (#47088) 2021-02-26 19:21:06 +00:00
Marcel van der Veldt d55f0df09a Fix Z-Wave JS discovery schema for thermostat devices (#47087)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-02-26 19:21:06 +00:00
Erik Montnemery 96e118ccfe Bump pychromecast to 8.1.2 (#47085) 2021-02-26 19:21:05 +00:00
Paulus Schoutsen 255b6faa7f Upgrade aiohttp to 3.7.4 (#47077) 2021-02-26 19:21:04 +00:00
rikroe 4cd40d0f9f Bump bimmer_connected to 0.7.15 and fix bugs (#47066)
Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2021-02-26 19:21:03 +00:00
J. Nick Koston c12769213d Add suggested area to hue (#47056) 2021-02-26 19:21:02 +00:00
CurrentThread 6a850a1481 Add support for Shelly SHBTN-2 device triggers (#46644) 2021-02-26 19:21:02 +00:00
Joakim Plate 101897c260 Add support for v6 features to philips js integration (#46422) 2021-02-26 19:21:01 +00:00
Paulus Schoutsen 6cdd6c3f44 Bumped version to 2021.3.0b2 2021-02-26 06:01:42 +00:00
Paulus Schoutsen 2c30579a11 Bump Z-Wave JS Server Python to 0.20.0 (#47076) 2021-02-26 06:01:35 +00:00
Raman Gupta ae0d301fd9 catch ValueError when unique ID update fails because its taken and remove the duplicate entity (#47072) 2021-02-26 06:01:34 +00:00
J. Nick Koston a7a66e8ddb Ensure hue options show the defaults when the config options have not yet been saved (#47067) 2021-02-26 06:01:33 +00:00
Bram Kragten 5228bbd43c Revert CORS changes for my home assistant (#47064)
* Revert CORS changes for my home assistant

* Update test_init.py

* Update test_init.py
2021-02-26 06:01:33 +00:00
Bram Kragten 35bce434cc Updated frontend to 20210225.0 (#47059) 2021-02-26 06:01:32 +00:00
60 changed files with 1262 additions and 356 deletions
+4 -2
View File
@@ -14,7 +14,7 @@ schedules:
always: true
variables:
- name: versionBuilder
value: '2020.11.0'
value: '2021.02.0'
- group: docker
- group: github
- group: twine
@@ -114,10 +114,12 @@ stages:
pool:
vmImage: 'ubuntu-latest'
strategy:
maxParallel: 15
maxParallel: 17
matrix:
qemux86-64:
buildMachine: 'qemux86-64'
generic-x86-64:
buildMachine: 'generic-x86-64'
intel-nuc:
buildMachine: 'intel-nuc'
qemux86:
-1
View File
@@ -178,7 +178,6 @@ class APIDiscoveryView(HomeAssistantView):
requires_auth = False
url = URL_API_DISCOVERY_INFO
name = "api:discovery"
cors_allowed = True
async def get(self, request):
"""Get discovery information."""
@@ -122,7 +122,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
def _update_all() -> None:
"""Update all BMW accounts."""
for entry in hass.data[DOMAIN][DATA_ENTRIES].values():
for entry in hass.data[DOMAIN][DATA_ENTRIES].copy().values():
entry[CONF_ACCOUNT].update()
# Add update listener for config entry changes (options)
@@ -42,12 +42,12 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity):
@property
def latitude(self):
"""Return latitude value of the device."""
return self._location[0]
return self._location[0] if self._location else None
@property
def longitude(self):
"""Return longitude value of the device."""
return self._location[1]
return self._location[1] if self._location else None
@property
def name(self):
@@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.7.14"],
"requirements": ["bimmer_connected==0.7.15"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true
}
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==8.1.0"],
"requirements": ["pychromecast==8.1.2"],
"after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"],
"zeroconf": ["_googlecast._tcp.local."],
"codeowners": ["@emontnemery"]
@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20210224.0"
"home-assistant-frontend==20210226.0"
],
"dependencies": [
"api",
@@ -3,7 +3,7 @@
"name": "HomeKit",
"documentation": "https://www.home-assistant.io/integrations/homekit",
"requirements": [
"HAP-python==3.3.0",
"HAP-python==3.3.1",
"fnvhash==0.1.0",
"PyQRCode==1.2.1",
"base36==0.1.1",
+1 -1
View File
@@ -59,7 +59,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_DEVELOPMENT = "0"
# Cast to be able to load custom cards.
# My to be able to check url and version info.
DEFAULT_CORS = ["https://cast.home-assistant.io", "https://my.home-assistant.io"]
DEFAULT_CORS = ["https://cast.home-assistant.io"]
NO_LOGIN_ATTEMPT_THRESHOLD = -1
MAX_CLIENT_SIZE: int = 1024 ** 2 * 16
+4 -2
View File
@@ -18,6 +18,8 @@ from .bridge import authenticate_bridge
from .const import ( # pylint: disable=unused-import
CONF_ALLOW_HUE_GROUPS,
CONF_ALLOW_UNREACHABLE,
DEFAULT_ALLOW_HUE_GROUPS,
DEFAULT_ALLOW_UNREACHABLE,
DOMAIN,
LOGGER,
)
@@ -246,13 +248,13 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow):
vol.Optional(
CONF_ALLOW_HUE_GROUPS,
default=self.config_entry.options.get(
CONF_ALLOW_HUE_GROUPS, False
CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS
),
): bool,
vol.Optional(
CONF_ALLOW_UNREACHABLE,
default=self.config_entry.options.get(
CONF_ALLOW_UNREACHABLE, False
CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE
),
): bool,
}
+6 -1
View File
@@ -12,6 +12,11 @@ CONF_ALLOW_UNREACHABLE = "allow_unreachable"
DEFAULT_ALLOW_UNREACHABLE = False
CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
DEFAULT_ALLOW_HUE_GROUPS = True
DEFAULT_ALLOW_HUE_GROUPS = False
DEFAULT_SCENE_TRANSITION = 4
GROUP_TYPE_LIGHT_GROUP = "LightGroup"
GROUP_TYPE_ROOM = "Room"
GROUP_TYPE_LUMINAIRE = "Luminaire"
GROUP_TYPE_LIGHT_SOURCE = "LightSource"
+115 -36
View File
@@ -36,7 +36,14 @@ from homeassistant.helpers.update_coordinator import (
)
from homeassistant.util import color
from .const import DOMAIN as HUE_DOMAIN, REQUEST_REFRESH_DELAY
from .const import (
DOMAIN as HUE_DOMAIN,
GROUP_TYPE_LIGHT_GROUP,
GROUP_TYPE_LIGHT_SOURCE,
GROUP_TYPE_LUMINAIRE,
GROUP_TYPE_ROOM,
REQUEST_REFRESH_DELAY,
)
from .helpers import remove_devices
SCAN_INTERVAL = timedelta(seconds=5)
@@ -74,24 +81,35 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"""
def create_light(item_class, coordinator, bridge, is_group, api, item_id):
def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id):
"""Create the light."""
api_item = api[item_id]
if is_group:
supported_features = 0
for light_id in api[item_id].lights:
for light_id in api_item.lights:
if light_id not in bridge.api.lights:
continue
light = bridge.api.lights[light_id]
supported_features |= SUPPORT_HUE.get(light.type, SUPPORT_HUE_EXTENDED)
supported_features = supported_features or SUPPORT_HUE_EXTENDED
else:
supported_features = SUPPORT_HUE.get(api[item_id].type, SUPPORT_HUE_EXTENDED)
return item_class(coordinator, bridge, is_group, api[item_id], supported_features)
supported_features = SUPPORT_HUE.get(api_item.type, SUPPORT_HUE_EXTENDED)
return item_class(
coordinator, bridge, is_group, api_item, supported_features, rooms
)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Hue lights from a config entry."""
bridge = hass.data[HUE_DOMAIN][config_entry.entry_id]
api_version = tuple(int(v) for v in bridge.api.config.apiversion.split("."))
rooms = {}
allow_groups = bridge.allow_groups
supports_groups = api_version >= GROUP_MIN_API_VERSION
if allow_groups and not supports_groups:
_LOGGER.warning("Please update your Hue bridge to support groups")
light_coordinator = DataUpdateCoordinator(
hass,
@@ -111,27 +129,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if not light_coordinator.last_update_success:
raise PlatformNotReady
update_lights = partial(
async_update_items,
bridge,
bridge.api.lights,
{},
async_add_entities,
partial(create_light, HueLight, light_coordinator, bridge, False),
)
# We add a listener after fetching the data, so manually trigger listener
bridge.reset_jobs.append(light_coordinator.async_add_listener(update_lights))
update_lights()
api_version = tuple(int(v) for v in bridge.api.config.apiversion.split("."))
allow_groups = bridge.allow_groups
if allow_groups and api_version < GROUP_MIN_API_VERSION:
_LOGGER.warning("Please update your Hue bridge to support groups")
allow_groups = False
if not allow_groups:
if not supports_groups:
update_lights_without_group_support = partial(
async_update_items,
bridge,
bridge.api.lights,
{},
async_add_entities,
partial(create_light, HueLight, light_coordinator, bridge, False, rooms),
None,
)
# We add a listener after fetching the data, so manually trigger listener
bridge.reset_jobs.append(
light_coordinator.async_add_listener(update_lights_without_group_support)
)
return
group_coordinator = DataUpdateCoordinator(
@@ -145,17 +156,69 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
),
)
update_groups = partial(
if allow_groups:
update_groups = partial(
async_update_items,
bridge,
bridge.api.groups,
{},
async_add_entities,
partial(create_light, HueLight, group_coordinator, bridge, True, None),
None,
)
bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups))
cancel_update_rooms_listener = None
@callback
def _async_update_rooms():
"""Update rooms."""
nonlocal cancel_update_rooms_listener
rooms.clear()
for item_id in bridge.api.groups:
group = bridge.api.groups[item_id]
if group.type != GROUP_TYPE_ROOM:
continue
for light_id in group.lights:
rooms[light_id] = group.name
# Once we do a rooms update, we cancel the listener
# until the next time lights are added
bridge.reset_jobs.remove(cancel_update_rooms_listener)
cancel_update_rooms_listener() # pylint: disable=not-callable
cancel_update_rooms_listener = None
@callback
def _setup_rooms_listener():
nonlocal cancel_update_rooms_listener
if cancel_update_rooms_listener is not None:
# If there are new lights added before _async_update_rooms
# is called we should not add another listener
return
cancel_update_rooms_listener = group_coordinator.async_add_listener(
_async_update_rooms
)
bridge.reset_jobs.append(cancel_update_rooms_listener)
_setup_rooms_listener()
await group_coordinator.async_refresh()
update_lights_with_group_support = partial(
async_update_items,
bridge,
bridge.api.groups,
bridge.api.lights,
{},
async_add_entities,
partial(create_light, HueLight, group_coordinator, bridge, True),
partial(create_light, HueLight, light_coordinator, bridge, False, rooms),
_setup_rooms_listener,
)
bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups))
await group_coordinator.async_refresh()
# We add a listener after fetching the data, so manually trigger listener
bridge.reset_jobs.append(
light_coordinator.async_add_listener(update_lights_with_group_support)
)
update_lights_with_group_support()
async def async_safe_fetch(bridge, fetch_method):
@@ -171,7 +234,9 @@ async def async_safe_fetch(bridge, fetch_method):
@callback
def async_update_items(bridge, api, current, async_add_entities, create_item):
def async_update_items(
bridge, api, current, async_add_entities, create_item, new_items_callback
):
"""Update items."""
new_items = []
@@ -185,6 +250,9 @@ def async_update_items(bridge, api, current, async_add_entities, create_item):
bridge.hass.async_create_task(remove_devices(bridge, api, current))
if new_items:
# This is currently used to setup the listener to update rooms
if new_items_callback:
new_items_callback()
async_add_entities(new_items)
@@ -201,13 +269,14 @@ def hass_to_hue_brightness(value):
class HueLight(CoordinatorEntity, LightEntity):
"""Representation of a Hue light."""
def __init__(self, coordinator, bridge, is_group, light, supported_features):
def __init__(self, coordinator, bridge, is_group, light, supported_features, rooms):
"""Initialize the light."""
super().__init__(coordinator)
self.light = light
self.bridge = bridge
self.is_group = is_group
self._supported_features = supported_features
self._rooms = rooms
if is_group:
self.is_osram = False
@@ -355,10 +424,15 @@ class HueLight(CoordinatorEntity, LightEntity):
@property
def device_info(self):
"""Return the device info."""
if self.light.type in ("LightGroup", "Room", "Luminaire", "LightSource"):
if self.light.type in (
GROUP_TYPE_LIGHT_GROUP,
GROUP_TYPE_ROOM,
GROUP_TYPE_LUMINAIRE,
GROUP_TYPE_LIGHT_SOURCE,
):
return None
return {
info = {
"identifiers": {(HUE_DOMAIN, self.device_id)},
"name": self.name,
"manufacturer": self.light.manufacturername,
@@ -370,6 +444,11 @@ class HueLight(CoordinatorEntity, LightEntity):
"via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid),
}
if self.light.id in self._rooms:
info["suggested_area"] = self._rooms[self.light.id]
return info
async def async_turn_on(self, **kwargs):
"""Turn the specified or all lights on."""
command = {"on": True}
@@ -44,6 +44,8 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity):
@property
def percentage(self) -> str:
"""Return the current speed percentage."""
if self._device["fan_speed"] is None:
return None
return ordered_list_item_to_percentage(
ORDERED_NAMED_FAN_SPEEDS, self._device["fan_speed"]
)
@@ -13,6 +13,7 @@ from homeassistant.const import (
CONF_DOMAIN,
CONF_PLATFORM,
CONF_TYPE,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -66,10 +67,11 @@ TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_AUTOMATION_TYPE): str,
vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD, default=None): vol.Any(None, cv.string),
vol.Required(CONF_TYPE): cv.string,
vol.Required(CONF_SUBTYPE): cv.string,
vol.Required(CONF_TOPIC): cv.string,
vol.Required(CONF_TYPE): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE, default=None): vol.Any(None, cv.string),
},
validate_device_has_at_least_one_identifier,
)
@@ -96,6 +98,8 @@ class TriggerInstance:
}
if self.trigger.payload:
mqtt_config[CONF_PAYLOAD] = self.trigger.payload
if self.trigger.value_template:
mqtt_config[CONF_VALUE_TEMPLATE] = self.trigger.value_template
mqtt_config = mqtt_trigger.TRIGGER_SCHEMA(mqtt_config)
if self.remove:
@@ -121,6 +125,7 @@ class Trigger:
subtype: str = attr.ib()
topic: str = attr.ib()
type: str = attr.ib()
value_template: str = attr.ib()
trigger_instances: List[TriggerInstance] = attr.ib(factory=list)
async def add_trigger(self, action, automation_info):
@@ -153,6 +158,7 @@ class Trigger:
self.qos = config[CONF_QOS]
topic_changed = self.topic != config[CONF_TOPIC]
self.topic = config[CONF_TOPIC]
self.value_template = config[CONF_VALUE_TEMPLATE]
# Unsubscribe+subscribe if this trigger is in use and topic has changed
# If topic is same unsubscribe+subscribe will execute in the wrong order
@@ -245,6 +251,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data):
payload=config[CONF_PAYLOAD],
qos=config[CONF_QOS],
remove_signal=remove_signal,
value_template=config[CONF_VALUE_TEMPLATE],
)
else:
await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger(
@@ -325,6 +332,7 @@ async def async_attach_trigger(
topic=None,
payload=None,
qos=None,
value_template=None,
)
return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger(
action, automation_info
+4 -2
View File
@@ -48,11 +48,13 @@ async def async_attach_trigger(hass, config, action, automation_info):
template.attach(hass, wanted_payload)
if wanted_payload:
wanted_payload = wanted_payload.async_render(variables, limited=True)
wanted_payload = wanted_payload.async_render(
variables, limited=True, parse_result=False
)
template.attach(hass, topic)
if isinstance(topic, template.Template):
topic = topic.async_render(variables, limited=True)
topic = topic.async_render(variables, limited=True, parse_result=False)
topic = mqtt.util.valid_subscribe_topic(topic)
template.attach(hass, value_template)
+1 -1
View File
@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["ffmpeg", "http"],
"documentation": "https://www.home-assistant.io/integrations/nest",
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.10"],
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.12"],
"codeowners": ["@allenporter"],
"quality_scale": "platinum",
"dhcp": [{"macaddress":"18B430*"}]
@@ -8,8 +8,13 @@ from haphilipsjs import ConnectionFailure, PhilipsTV
from homeassistant.components.automation import AutomationActionType
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_VERSION, CONF_HOST
from homeassistant.core import Context, HassJob, HomeAssistant, callback
from homeassistant.const import (
CONF_API_VERSION,
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
)
from homeassistant.core import CALLBACK_TYPE, Context, HassJob, HomeAssistant, callback
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -30,7 +35,12 @@ async def async_setup(hass: HomeAssistant, config: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Philips TV from a config entry."""
tvapi = PhilipsTV(entry.data[CONF_HOST], entry.data[CONF_API_VERSION])
tvapi = PhilipsTV(
entry.data[CONF_HOST],
entry.data[CONF_API_VERSION],
username=entry.data.get(CONF_USERNAME),
password=entry.data.get(CONF_PASSWORD),
)
coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi)
@@ -103,7 +113,9 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
def __init__(self, hass, api: PhilipsTV) -> None:
"""Set up the coordinator."""
self.api = api
self._notify_future: Optional[asyncio.Task] = None
@callback
def _update_listeners():
for update_callback in self._listeners:
update_callback()
@@ -120,9 +132,43 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
),
)
async def _notify_task(self):
while self.api.on and self.api.notify_change_supported:
if await self.api.notifyChange(130):
self.async_set_updated_data(None)
@callback
def _async_notify_stop(self):
if self._notify_future:
self._notify_future.cancel()
self._notify_future = None
@callback
def _async_notify_schedule(self):
if (
(self._notify_future is None or self._notify_future.done())
and self.api.on
and self.api.notify_change_supported
):
self._notify_future = self.hass.loop.create_task(self._notify_task())
@callback
def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None:
"""Remove data update."""
super().async_remove_listener(update_callback)
if not self._listeners:
self._async_notify_stop()
@callback
def _async_stop_refresh(self, event: asyncio.Event) -> None:
super()._async_stop_refresh(event)
self._async_notify_stop()
@callback
async def _async_update_data(self):
"""Fetch the latest data from the source."""
try:
await self.hass.async_add_executor_job(self.api.update)
await self.api.update()
self._async_notify_schedule()
except ConnectionFailure:
pass
@@ -1,35 +1,47 @@
"""Config flow for Philips TV integration."""
import logging
from typing import Any, Dict, Optional, TypedDict
import platform
from typing import Any, Dict, Optional, Tuple, TypedDict
from haphilipsjs import ConnectionFailure, PhilipsTV
from haphilipsjs import ConnectionFailure, PairingFailure, PhilipsTV
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.const import CONF_API_VERSION, CONF_HOST
from homeassistant.const import (
CONF_API_VERSION,
CONF_HOST,
CONF_PASSWORD,
CONF_PIN,
CONF_USERNAME,
)
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
from . import LOGGER
from .const import ( # pylint:disable=unused-import
CONF_SYSTEM,
CONST_APP_ID,
CONST_APP_NAME,
DOMAIN,
)
class FlowUserDict(TypedDict):
"""Data for user step."""
host: str
api_version: int
async def validate_input(hass: core.HomeAssistant, data: FlowUserDict):
async def validate_input(
hass: core.HomeAssistant, host: str, api_version: int
) -> Tuple[Dict, PhilipsTV]:
"""Validate the user input allows us to connect."""
hub = PhilipsTV(data[CONF_HOST], data[CONF_API_VERSION])
hub = PhilipsTV(host, api_version)
await hass.async_add_executor_job(hub.getSystem)
await hub.getSystem()
await hub.setTransport(hub.secured_transport)
if hub.system is None:
raise ConnectionFailure
if not hub.system:
raise ConnectionFailure("System data is empty")
return hub.system
return hub
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@@ -38,7 +50,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
_default = {}
_current = {}
_hub: PhilipsTV
_pair_state: Any
async def async_step_import(self, conf: Dict[str, Any]):
"""Import a configuration from config.yaml."""
@@ -53,34 +67,99 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
}
)
async def _async_create_current(self):
system = self._current[CONF_SYSTEM]
return self.async_create_entry(
title=f"{system['name']} ({system['serialnumber']})",
data=self._current,
)
async def async_step_pair(self, user_input: Optional[Dict] = None):
"""Attempt to pair with device."""
assert self._hub
errors = {}
schema = vol.Schema(
{
vol.Required(CONF_PIN): str,
}
)
if not user_input:
try:
self._pair_state = await self._hub.pairRequest(
CONST_APP_ID,
CONST_APP_NAME,
platform.node(),
platform.system(),
"native",
)
except PairingFailure as exc:
LOGGER.debug(str(exc))
return self.async_abort(
reason="pairing_failure",
description_placeholders={"error_id": exc.data.get("error_id")},
)
return self.async_show_form(
step_id="pair", data_schema=schema, errors=errors
)
try:
username, password = await self._hub.pairGrant(
self._pair_state, user_input[CONF_PIN]
)
except PairingFailure as exc:
LOGGER.debug(str(exc))
if exc.data.get("error_id") == "INVALID_PIN":
errors[CONF_PIN] = "invalid_pin"
return self.async_show_form(
step_id="pair", data_schema=schema, errors=errors
)
return self.async_abort(
reason="pairing_failure",
description_placeholders={"error_id": exc.data.get("error_id")},
)
self._current[CONF_USERNAME] = username
self._current[CONF_PASSWORD] = password
return await self._async_create_current()
async def async_step_user(self, user_input: Optional[FlowUserDict] = None):
"""Handle the initial step."""
errors = {}
if user_input:
self._default = user_input
self._current = user_input
try:
system = await validate_input(self.hass, user_input)
except ConnectionFailure:
hub = await validate_input(
self.hass, user_input[CONF_HOST], user_input[CONF_API_VERSION]
)
except ConnectionFailure as exc:
LOGGER.error(str(exc))
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(system["serialnumber"])
self._abort_if_unique_id_configured(updates=user_input)
data = {**user_input, "system": system}
await self.async_set_unique_id(hub.system["serialnumber"])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"{system['name']} ({system['serialnumber']})", data=data
)
self._current[CONF_SYSTEM] = hub.system
self._current[CONF_API_VERSION] = hub.api_version
self._hub = hub
if hub.pairing_type == "digest_auth_pairing":
return await self.async_step_pair()
return await self._async_create_current()
schema = vol.Schema(
{
vol.Required(CONF_HOST, default=self._default.get(CONF_HOST)): str,
vol.Required(CONF_HOST, default=self._current.get(CONF_HOST)): str,
vol.Required(
CONF_API_VERSION, default=self._default.get(CONF_API_VERSION)
): vol.In([1, 6]),
CONF_API_VERSION, default=self._current.get(CONF_API_VERSION, 1)
): vol.In([1, 5, 6]),
}
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
@@ -2,3 +2,6 @@
DOMAIN = "philips_js"
CONF_SYSTEM = "system"
CONST_APP_ID = "homeassistant.io"
CONST_APP_NAME = "Home Assistant"
@@ -3,7 +3,7 @@
"name": "Philips TV",
"documentation": "https://www.home-assistant.io/integrations/philips_js",
"requirements": [
"ha-philipsjs==0.1.0"
"ha-philipsjs==2.3.0"
],
"codeowners": [
"@elupus"
@@ -1,6 +1,7 @@
"""Media Player component to integrate TVs exposing the Joint Space API."""
from typing import Any, Dict
from typing import Any, Dict, Optional
from haphilipsjs import ConnectionFailure
import voluptuous as vol
from homeassistant import config_entries
@@ -11,15 +12,21 @@ from homeassistant.components.media_player import (
MediaPlayerEntity,
)
from homeassistant.components.media_player.const import (
MEDIA_CLASS_APP,
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_APP,
MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
@@ -27,7 +34,6 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_STEP,
)
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.philips_js import PhilipsTVDataUpdateCoordinator
from homeassistant.const import (
CONF_API_VERSION,
CONF_HOST,
@@ -40,7 +46,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import LOGGER as _LOGGER
from . import LOGGER as _LOGGER, PhilipsTVDataUpdateCoordinator
from .const import CONF_SYSTEM, DOMAIN
SUPPORT_PHILIPS_JS = (
@@ -53,16 +59,15 @@ SUPPORT_PHILIPS_JS = (
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_PLAY_MEDIA
| SUPPORT_BROWSE_MEDIA
| SUPPORT_PLAY
| SUPPORT_PAUSE
| SUPPORT_STOP
)
CONF_ON_ACTION = "turn_on_action"
DEFAULT_API_VERSION = 1
PREFIX_SEPARATOR = ": "
PREFIX_SOURCE = "Input"
PREFIX_CHANNEL = "Channel"
PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_HOST),
cv.deprecated(CONF_NAME),
@@ -131,12 +136,19 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
self._supports = SUPPORT_PHILIPS_JS
self._system = system
self._unique_id = unique_id
self._state = STATE_OFF
self._media_content_type: Optional[str] = None
self._media_content_id: Optional[str] = None
self._media_title: Optional[str] = None
self._media_channel: Optional[str] = None
super().__init__(coordinator)
self._update_from_coordinator()
def _update_soon(self):
async def _async_update_soon(self):
"""Reschedule update task."""
self.hass.add_job(self.coordinator.async_request_refresh)
self.async_write_ha_state()
await self.coordinator.async_request_refresh()
@property
def name(self):
@@ -147,7 +159,9 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
def supported_features(self):
"""Flag media player features that are supported."""
supports = self._supports
if self._coordinator.turn_on:
if self._coordinator.turn_on or (
self._tv.on and self._tv.powerstate is not None
):
supports |= SUPPORT_TURN_ON
return supports
@@ -155,7 +169,8 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
def state(self):
"""Get the device state. An exception means OFF state."""
if self._tv.on:
return STATE_ON
if self._tv.powerstate == "On" or self._tv.powerstate is None:
return STATE_ON
return STATE_OFF
@property
@@ -168,22 +183,12 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
"""List of available input sources."""
return list(self._sources.values())
def select_source(self, source):
async def async_select_source(self, source):
"""Set the input source."""
data = source.split(PREFIX_SEPARATOR, 1)
if data[0] == PREFIX_SOURCE: # Legacy way to set source
source_id = _inverted(self._sources).get(data[1])
if source_id:
self._tv.setSource(source_id)
elif data[0] == PREFIX_CHANNEL: # Legacy way to set channel
channel_id = _inverted(self._channels).get(data[1])
if channel_id:
self._tv.setChannel(channel_id)
else:
source_id = _inverted(self._sources).get(source)
if source_id:
self._tv.setSource(source_id)
self._update_soon()
source_id = _inverted(self._sources).get(source)
if source_id:
await self._tv.setSource(source_id)
await self._async_update_soon()
@property
def volume_level(self):
@@ -197,78 +202,118 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
async def async_turn_on(self):
"""Turn on the device."""
await self._coordinator.turn_on.async_run(self.hass, self._context)
if self._tv.on and self._tv.powerstate:
await self._tv.setPowerState("On")
self._state = STATE_ON
else:
await self._coordinator.turn_on.async_run(self.hass, self._context)
await self._async_update_soon()
def turn_off(self):
async def async_turn_off(self):
"""Turn off the device."""
self._tv.sendKey("Standby")
self._tv.on = False
self._update_soon()
await self._tv.sendKey("Standby")
self._state = STATE_OFF
await self._async_update_soon()
def volume_up(self):
async def async_volume_up(self):
"""Send volume up command."""
self._tv.sendKey("VolumeUp")
self._update_soon()
await self._tv.sendKey("VolumeUp")
await self._async_update_soon()
def volume_down(self):
async def async_volume_down(self):
"""Send volume down command."""
self._tv.sendKey("VolumeDown")
self._update_soon()
await self._tv.sendKey("VolumeDown")
await self._async_update_soon()
def mute_volume(self, mute):
async def async_mute_volume(self, mute):
"""Send mute command."""
self._tv.setVolume(None, mute)
self._update_soon()
if self._tv.muted != mute:
await self._tv.sendKey("Mute")
await self._async_update_soon()
else:
_LOGGER.debug("Ignoring request when already in expected state")
def set_volume_level(self, volume):
async def async_set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self._tv.setVolume(volume, self._tv.muted)
self._update_soon()
await self._tv.setVolume(volume, self._tv.muted)
await self._async_update_soon()
def media_previous_track(self):
async def async_media_previous_track(self):
"""Send rewind command."""
self._tv.sendKey("Previous")
self._update_soon()
await self._tv.sendKey("Previous")
await self._async_update_soon()
def media_next_track(self):
async def async_media_next_track(self):
"""Send fast forward command."""
self._tv.sendKey("Next")
self._update_soon()
await self._tv.sendKey("Next")
await self._async_update_soon()
async def async_media_play_pause(self):
"""Send pause command to media player."""
if self._tv.quirk_playpause_spacebar:
await self._tv.sendUnicode(" ")
else:
await self._tv.sendKey("PlayPause")
await self._async_update_soon()
async def async_media_play(self):
"""Send pause command to media player."""
await self._tv.sendKey("Play")
await self._async_update_soon()
async def async_media_pause(self):
"""Send play command to media player."""
await self._tv.sendKey("Pause")
await self._async_update_soon()
async def async_media_stop(self):
"""Send play command to media player."""
await self._tv.sendKey("Stop")
await self._async_update_soon()
@property
def media_channel(self):
"""Get current channel if it's a channel."""
if self.media_content_type == MEDIA_TYPE_CHANNEL:
return self._channels.get(self._tv.channel_id)
return None
return self._media_channel
@property
def media_title(self):
"""Title of current playing media."""
if self.media_content_type == MEDIA_TYPE_CHANNEL:
return self._channels.get(self._tv.channel_id)
return self._sources.get(self._tv.source_id)
return self._media_title
@property
def media_content_type(self):
"""Return content type of playing media."""
if self._tv.source_id == "tv" or self._tv.source_id == "11":
return MEDIA_TYPE_CHANNEL
if self._tv.source_id is None and self._tv.channels:
return MEDIA_TYPE_CHANNEL
return None
return self._media_content_type
@property
def media_content_id(self):
"""Content type of current playing media."""
if self.media_content_type == MEDIA_TYPE_CHANNEL:
return self._channels.get(self._tv.channel_id)
return self._media_content_id
@property
def media_image_url(self):
"""Image url of current playing media."""
if self._media_content_id and self._media_content_type in (
MEDIA_CLASS_APP,
MEDIA_CLASS_CHANNEL,
):
return self.get_browse_image_url(
self._media_content_type, self._media_content_id, media_image_id=None
)
return None
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {"channel_list": list(self._channels.values())}
def app_id(self):
"""ID of the current running app."""
return self._tv.application_id
@property
def app_name(self):
"""Name of the current running app."""
app = self._tv.applications.get(self._tv.application_id)
if app:
return app.get("label")
@property
def device_class(self):
@@ -293,57 +338,243 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
"sw_version": self._system.get("softwareversion"),
}
def play_media(self, media_type, media_id, **kwargs):
async def async_play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
_LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id)
if media_type == MEDIA_TYPE_CHANNEL:
channel_id = _inverted(self._channels).get(media_id)
list_id, _, channel_id = media_id.partition("/")
if channel_id:
self._tv.setChannel(channel_id)
self._update_soon()
await self._tv.setChannel(channel_id, list_id)
await self._async_update_soon()
else:
_LOGGER.error("Unable to find channel <%s>", media_id)
elif media_type == MEDIA_TYPE_APP:
app = self._tv.applications.get(media_id)
if app:
await self._tv.setApplication(app["intent"])
await self._async_update_soon()
else:
_LOGGER.error("Unable to find application <%s>", media_id)
else:
_LOGGER.error("Unsupported media type <%s>", media_type)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
if media_content_id not in (None, ""):
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
)
async def async_browse_media_channels(self, expanded):
"""Return channel media objects."""
if expanded:
children = [
BrowseMedia(
title=channel.get("name", f"Channel: {channel_id}"),
media_class=MEDIA_CLASS_CHANNEL,
media_content_id=f"alltv/{channel_id}",
media_content_type=MEDIA_TYPE_CHANNEL,
can_play=True,
can_expand=False,
thumbnail=self.get_browse_image_url(
MEDIA_TYPE_APP, channel_id, media_image_id=None
),
)
for channel_id, channel in self._tv.channels.items()
]
else:
children = None
return BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="",
media_content_id="channels",
media_content_type=MEDIA_TYPE_CHANNELS,
children_media_class=MEDIA_TYPE_CHANNEL,
can_play=False,
can_expand=True,
children=children,
)
async def async_browse_media_favorites(self, list_id, expanded):
"""Return channel media objects."""
if expanded:
favorites = await self._tv.getFavoriteList(list_id)
if favorites:
def get_name(channel):
channel_data = self._tv.channels.get(str(channel["ccid"]))
if channel_data:
return channel_data["name"]
return f"Channel: {channel['ccid']}"
children = [
BrowseMedia(
title=get_name(channel),
media_class=MEDIA_CLASS_CHANNEL,
media_content_id=f"{list_id}/{channel['ccid']}",
media_content_type=MEDIA_TYPE_CHANNEL,
can_play=True,
can_expand=False,
thumbnail=self.get_browse_image_url(
MEDIA_TYPE_APP, channel, media_image_id=None
),
)
for channel in favorites
]
else:
children = None
else:
children = None
favorite = self._tv.favorite_lists[list_id]
return BrowseMedia(
title=favorite.get("name", f"Favorites {list_id}"),
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id=f"favorites/{list_id}",
media_content_type=MEDIA_TYPE_CHANNELS,
children_media_class=MEDIA_TYPE_CHANNEL,
can_play=False,
can_expand=True,
children=children,
)
async def async_browse_media_applications(self, expanded):
"""Return application media objects."""
if expanded:
children = [
BrowseMedia(
title=application["label"],
media_class=MEDIA_CLASS_APP,
media_content_id=application_id,
media_content_type=MEDIA_TYPE_APP,
can_play=True,
can_expand=False,
thumbnail=self.get_browse_image_url(
MEDIA_TYPE_APP, application_id, media_image_id=None
),
)
for application_id, application in self._tv.applications.items()
]
else:
children = None
return BrowseMedia(
title="Applications",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="applications",
media_content_type=MEDIA_TYPE_APPS,
children_media_class=MEDIA_TYPE_APP,
can_play=False,
can_expand=True,
children=children,
)
async def async_browse_media_favorite_lists(self, expanded):
"""Return favorite media objects."""
if self._tv.favorite_lists and expanded:
children = [
await self.async_browse_media_favorites(list_id, False)
for list_id in self._tv.favorite_lists
]
else:
children = None
return BrowseMedia(
title="Favorites",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="favorite_lists",
media_content_type=MEDIA_TYPE_CHANNELS,
children_media_class=MEDIA_TYPE_CHANNEL,
can_play=False,
can_expand=True,
children=children,
)
async def async_browse_media_root(self):
"""Return root media objects."""
return BrowseMedia(
title="Library",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="",
media_content_type="",
can_play=False,
can_expand=True,
children=[
BrowseMedia(
title=channel,
media_class=MEDIA_CLASS_CHANNEL,
media_content_id=channel,
media_content_type=MEDIA_TYPE_CHANNEL,
can_play=True,
can_expand=False,
)
for channel in self._channels.values()
await self.async_browse_media_channels(False),
await self.async_browse_media_applications(False),
await self.async_browse_media_favorite_lists(False),
],
)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
if not self._tv.on:
raise BrowseError("Can't browse when tv is turned off")
if media_content_id in (None, ""):
return await self.async_browse_media_root()
path = media_content_id.partition("/")
if path[0] == "channels":
return await self.async_browse_media_channels(True)
if path[0] == "applications":
return await self.async_browse_media_applications(True)
if path[0] == "favorite_lists":
return await self.async_browse_media_favorite_lists(True)
if path[0] == "favorites":
return await self.async_browse_media_favorites(path[2], True)
raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
async def async_get_browse_image(
self, media_content_type, media_content_id, media_image_id=None
):
"""Serve album art. Returns (content, content_type)."""
try:
if media_content_type == MEDIA_TYPE_APP and media_content_id:
return await self._tv.getApplicationIcon(media_content_id)
if media_content_type == MEDIA_TYPE_CHANNEL and media_content_id:
return await self._tv.getChannelLogo(media_content_id)
except ConnectionFailure:
_LOGGER.warning("Failed to fetch image")
return None, None
async def async_get_media_image(self):
"""Serve album art. Returns (content, content_type)."""
return await self.async_get_browse_image(
self.media_content_type, self.media_content_id, None
)
@callback
def _update_from_coordinator(self):
if self._tv.on:
if self._tv.powerstate in ("Standby", "StandbyKeep"):
self._state = STATE_OFF
else:
self._state = STATE_ON
else:
self._state = STATE_OFF
self._sources = {
srcid: source.get("name") or f"Source {srcid}"
for srcid, source in (self._tv.sources or {}).items()
}
self._channels = {
chid: channel.get("name") or f"Channel {chid}"
for chid, channel in (self._tv.channels or {}).items()
}
if self._tv.channel_active:
self._media_content_type = MEDIA_TYPE_CHANNEL
self._media_content_id = f"all/{self._tv.channel_id}"
self._media_title = self._tv.channels.get(self._tv.channel_id, {}).get(
"name"
)
self._media_channel = self._media_title
elif self._tv.application_id:
self._media_content_type = MEDIA_TYPE_APP
self._media_content_id = self._tv.application_id
self._media_title = self._tv.applications.get(
self._tv.application_id, {}
).get("label")
self._media_channel = None
else:
self._media_content_type = None
self._media_content_id = None
self._media_title = self._sources.get(self._tv.source_id)
self._media_channel = None
@callback
def _handle_coordinator_update(self) -> None:
@@ -10,8 +10,10 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"unknown": "[%key:common::config_flow::error::unknown%]",
"pairing_failure": "Unable to pair: {error_id}",
"invalid_pin": "Invalid PIN"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
@@ -5,7 +5,9 @@
},
"error": {
"cannot_connect": "Failed to connect",
"unknown": "Unexpected error"
"unknown": "Unexpected error",
"pairing_failure": "Unable to pair: {error_id}",
"invalid_pin": "Invalid PIN"
},
"step": {
"user": {
+2 -2
View File
@@ -74,5 +74,5 @@ INPUTS_EVENTS_SUBTYPES = {
# Kelvin value for colorTemp
KELVIN_MAX_VALUE = 6500
KELVIN_MIN_VALUE = 2700
KELVIN_MIN_VALUE_SHBLB_1 = 3000
KELVIN_MIN_VALUE_WHITE = 2700
KELVIN_MIN_VALUE_COLOR = 3000
+20 -27
View File
@@ -28,20 +28,13 @@ from .const import (
DATA_CONFIG_ENTRY,
DOMAIN,
KELVIN_MAX_VALUE,
KELVIN_MIN_VALUE,
KELVIN_MIN_VALUE_SHBLB_1,
KELVIN_MIN_VALUE_COLOR,
KELVIN_MIN_VALUE_WHITE,
)
from .entity import ShellyBlockEntity
from .utils import async_remove_shelly_entity
def min_kelvin(model: str):
"""Kelvin (min) for colorTemp."""
if model in ["SHBLB-1"]:
return KELVIN_MIN_VALUE_SHBLB_1
return KELVIN_MIN_VALUE
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up lights for device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP]
@@ -76,6 +69,8 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
self.control_result = None
self.mode_result = None
self._supported_features = 0
self._min_kelvin = KELVIN_MIN_VALUE_WHITE
self._max_kelvin = KELVIN_MAX_VALUE
if hasattr(block, "brightness") or hasattr(block, "gain"):
self._supported_features |= SUPPORT_BRIGHTNESS
@@ -85,6 +80,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
self._supported_features |= SUPPORT_WHITE_VALUE
if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"):
self._supported_features |= SUPPORT_COLOR
self._min_kelvin = KELVIN_MIN_VALUE_COLOR
@property
def supported_features(self) -> int:
@@ -168,22 +164,19 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
else:
color_temp = self.block.colorTemp
# If you set DUO to max mireds in Shelly app, 2700K,
# It reports 0 temp
if color_temp == 0:
return min_kelvin(self.wrapper.model)
color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp))
return int(color_temperature_kelvin_to_mired(color_temp))
@property
def min_mireds(self) -> int:
"""Return the coldest color_temp that this light supports."""
return int(color_temperature_kelvin_to_mired(KELVIN_MAX_VALUE))
return int(color_temperature_kelvin_to_mired(self._max_kelvin))
@property
def max_mireds(self) -> int:
"""Return the warmest color_temp that this light supports."""
return int(color_temperature_kelvin_to_mired(min_kelvin(self.wrapper.model)))
return int(color_temperature_kelvin_to_mired(self._min_kelvin))
async def async_turn_on(self, **kwargs) -> None:
"""Turn on light."""
@@ -192,6 +185,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
self.async_write_ha_state()
return
set_mode = None
params = {"turn": "on"}
if ATTR_BRIGHTNESS in kwargs:
tmp_brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100)
@@ -201,27 +195,26 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
params["brightness"] = tmp_brightness
if ATTR_COLOR_TEMP in kwargs:
color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])
color_temp = min(
KELVIN_MAX_VALUE, max(min_kelvin(self.wrapper.model), color_temp)
)
color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp))
# Color temperature change - used only in white mode, switch device mode to white
if self.mode == "color":
self.mode_result = await self.wrapper.device.switch_light_mode("white")
params["red"] = params["green"] = params["blue"] = 255
set_mode = "white"
params["red"] = params["green"] = params["blue"] = 255
params["temp"] = int(color_temp)
elif ATTR_HS_COLOR in kwargs:
if ATTR_HS_COLOR in kwargs:
red, green, blue = color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
# Color channels change - used only in color mode, switch device mode to color
if self.mode == "white":
self.mode_result = await self.wrapper.device.switch_light_mode("color")
set_mode = "color"
params["red"] = red
params["green"] = green
params["blue"] = blue
elif ATTR_WHITE_VALUE in kwargs:
if ATTR_WHITE_VALUE in kwargs:
# White channel change - used only in color mode, switch device mode device to color
if self.mode == "white":
self.mode_result = await self.wrapper.device.switch_light_mode("color")
set_mode = "color"
params["white"] = int(kwargs[ATTR_WHITE_VALUE])
if set_mode and self.mode != set_mode:
self.mode_result = await self.wrapper.device.switch_light_mode(set_mode)
self.control_result = await self.block.set_state(**params)
self.async_write_ha_state()
@@ -3,7 +3,7 @@
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==0.6.0"],
"requirements": ["aioshelly==0.6.1"],
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
"codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"]
}
+2 -2
View File
@@ -111,7 +111,7 @@ def get_device_channel_name(
def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool:
"""Return true if input button settings is set to a momentary type."""
# Shelly Button type is fixed to momentary and no btn_type
if settings["device"]["type"] == "SHBTN-1":
if settings["device"]["type"] in ("SHBTN-1", "SHBTN-2"):
return True
button = settings.get("relays") or settings.get("lights") or settings.get("inputs")
@@ -158,7 +158,7 @@ def get_input_triggers(
else:
subtype = f"button{int(block.channel)+1}"
if device.settings["device"]["type"] == "SHBTN-1":
if device.settings["device"]["type"] in ("SHBTN-1", "SHBTN-2"):
trigger_types = SHBTN_1_INPUTS_EVENTS_TYPES
elif device.settings["device"]["type"] == "SHIX3-1":
trigger_types = SHIX3_1_INPUTS_EVENTS_TYPES
+4 -4
View File
@@ -27,7 +27,7 @@ UPDATER_URL = "https://updater.home-assistant.io/"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: {
vol.Optional(DOMAIN, default={}): {
vol.Optional(CONF_REPORTING, default=True): cv.boolean,
vol.Optional(CONF_COMPONENT_REPORTING, default=False): cv.boolean,
}
@@ -56,13 +56,13 @@ async def async_setup(hass, config):
# This component only makes sense in release versions
_LOGGER.info("Running on 'dev', only analytics will be submitted")
conf = config.get(DOMAIN, {})
if conf.get(CONF_REPORTING):
conf = config[DOMAIN]
if conf[CONF_REPORTING]:
huuid = await hass.helpers.instance_id.async_get()
else:
huuid = None
include_components = conf.get(CONF_COMPONENT_REPORTING)
include_components = conf[CONF_COMPONENT_REPORTING]
async def check_new_version() -> Updater:
"""Check if a new version is available and report if one is."""
+1 -1
View File
@@ -7,7 +7,7 @@
"bellows==0.21.0",
"pyserial==3.5",
"pyserial-asyncio==0.5",
"zha-quirks==0.0.53",
"zha-quirks==0.0.54",
"zigpy-cc==0.5.2",
"zigpy-deconz==0.11.1",
"zigpy==0.32.0",
+14 -4
View File
@@ -95,10 +95,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
old_unique_id,
new_unique_id,
)
ent_reg.async_update_entity(
entity_id,
new_unique_id=new_unique_id,
)
try:
ent_reg.async_update_entity(
entity_id,
new_unique_id=new_unique_id,
)
except ValueError:
LOGGER.debug(
(
"Entity %s can't be migrated because the unique ID is taken. "
"Cleaning it up since it is likely no longer valid."
),
entity_id,
)
ent_reg.async_remove(entity_id)
@callback
def async_on_node_ready(node: ZwaveNode) -> None:
+18 -15
View File
@@ -75,6 +75,8 @@ class ZWaveDiscoverySchema:
device_class_specific: Optional[Set[Union[str, int]]] = None
# [optional] additional values that ALL need to be present on the node for this scheme to pass
required_values: Optional[List[ZWaveValueDiscoverySchema]] = None
# [optional] additional values that MAY NOT be present on the node for this scheme to pass
absent_values: Optional[List[ZWaveValueDiscoverySchema]] = None
# [optional] bool to specify if this primary value may be discovered by multiple platforms
allow_multi: bool = False
@@ -186,36 +188,30 @@ DISCOVERY_SCHEMAS = [
),
),
# climate
# thermostats supporting mode (and optional setpoint)
ZWaveDiscoverySchema(
platform="climate",
device_class_generic={"Thermostat"},
device_class_specific={
"Setback Thermostat",
"Thermostat General",
"Thermostat General V2",
"General Thermostat",
"General Thermostat V2",
},
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.THERMOSTAT_MODE},
property={"mode"},
type={"number"},
),
),
# climate
# setpoint thermostats
# thermostats supporting setpoint only (and thus not mode)
ZWaveDiscoverySchema(
platform="climate",
device_class_generic={"Thermostat"},
device_class_specific={
"Setpoint Thermostat",
"Unused",
},
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.THERMOSTAT_SETPOINT},
property={"setpoint"},
type={"number"},
),
absent_values=[ # mode must not be present to prevent dupes
ZWaveValueDiscoverySchema(
command_class={CommandClass.THERMOSTAT_MODE},
property={"mode"},
type={"number"},
),
],
),
# binary sensors
ZWaveDiscoverySchema(
@@ -436,6 +432,13 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None
for val_scheme in schema.required_values
):
continue
# check for values that may not be present
if schema.absent_values is not None:
if any(
any(check_value(val, val_scheme) for val in node.values.values())
for val_scheme in schema.absent_values
):
continue
# all checks passed, this value belongs to an entity
yield ZwaveDiscoveryInfo(
node=value.node,
@@ -3,7 +3,7 @@
"name": "Z-Wave JS",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
"requirements": ["zwave-js-server-python==0.19.0"],
"requirements": ["zwave-js-server-python==0.20.0"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["http", "websocket_api"]
}
+1 -1
View File
@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 2021
MINOR_VERSION = 3
PATCH_VERSION = "0b1"
PATCH_VERSION = "0b4"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 8, 0)
+2 -2
View File
@@ -1,6 +1,6 @@
PyJWT==1.7.1
PyNaCl==1.3.0
aiohttp==3.7.3
aiohttp==3.7.4
aiohttp_cors==0.7.0
astral==1.10.1
async-upnp-client==0.14.13
@@ -15,7 +15,7 @@ defusedxml==0.6.0
distro==1.5.0
emoji==1.2.0
hass-nabucasa==0.41.0
home-assistant-frontend==20210224.0
home-assistant-frontend==20210226.0
httpx==0.16.1
jinja2>=2.11.3
netdisco==2.8.2
+2 -2
View File
@@ -19,7 +19,7 @@ def ordered_list_item_to_percentage(ordered_list: List[str], item: str) -> int:
"""
if item not in ordered_list:
raise ValueError
raise ValueError(f'The item "{item}"" is not in "{ordered_list}"')
list_len = len(ordered_list)
list_position = ordered_list.index(item) + 1
@@ -42,7 +42,7 @@ def percentage_to_ordered_list_item(ordered_list: List[str], percentage: int) ->
"""
list_len = len(ordered_list)
if not list_len:
raise ValueError
raise ValueError("The ordered list is empty")
for offset, speed in enumerate(ordered_list):
list_position = offset + 1
+34
View File
@@ -0,0 +1,34 @@
ARG BUILD_VERSION
FROM agners/amd64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
libva-intel-driver \
usbutils
##
# Build libcec for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
p8-platform-dev \
linux-headers \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& cd /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
-DHAVE_LINUX_API=1 \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec*
+3
View File
@@ -1,6 +1,9 @@
ARG BUILD_VERSION
FROM homeassistant/amd64-homeassistant:$BUILD_VERSION
# NOTE: intel-nuc will be replaced by generic-x86-64. Make sure to apply
# changes in generic-x86-64 as well.
RUN apk --no-cache add \
libva-intel-driver \
usbutils
+1 -1
View File
@@ -1,7 +1,7 @@
-c homeassistant/package_constraints.txt
# Home Assistant Core
aiohttp==3.7.3
aiohttp==3.7.4
astral==1.10.1
async_timeout==3.0.1
attrs==19.3.0
+9 -9
View File
@@ -17,7 +17,7 @@ Adafruit-SHT31==1.0.2
# Adafruit_BBIO==1.1.1
# homeassistant.components.homekit
HAP-python==3.3.0
HAP-python==3.3.1
# homeassistant.components.mastodon
Mastodon.py==1.5.1
@@ -221,7 +221,7 @@ aiopylgtv==0.3.3
aiorecollect==1.0.1
# homeassistant.components.shelly
aioshelly==0.6.0
aioshelly==0.6.1
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -343,7 +343,7 @@ beautifulsoup4==4.9.3
bellows==0.21.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.14
bimmer_connected==0.7.15
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@@ -682,7 +682,7 @@ google-cloud-pubsub==2.1.0
google-cloud-texttospeech==0.4.0
# homeassistant.components.nest
google-nest-sdm==0.2.10
google-nest-sdm==0.2.12
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@@ -721,7 +721,7 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
ha-philipsjs==0.1.0
ha-philipsjs==2.3.0
# homeassistant.components.habitica
habitipy==0.2.0
@@ -763,7 +763,7 @@ hole==0.5.1
holidays==0.10.5.2
# homeassistant.components.frontend
home-assistant-frontend==20210224.0
home-assistant-frontend==20210226.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -1305,7 +1305,7 @@ pycfdns==1.2.1
pychannels==1.0.0
# homeassistant.components.cast
pychromecast==8.1.0
pychromecast==8.1.2
# homeassistant.components.pocketcasts
pycketcasts==1.0.0
@@ -2367,7 +2367,7 @@ zengge==0.2
zeroconf==0.28.8
# homeassistant.components.zha
zha-quirks==0.0.53
zha-quirks==0.0.54
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -2397,4 +2397,4 @@ zigpy==0.32.0
zm-py==0.5.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.19.0
zwave-js-server-python==0.20.0
+9 -9
View File
@@ -7,7 +7,7 @@
AEMET-OpenData==0.1.8
# homeassistant.components.homekit
HAP-python==3.3.0
HAP-python==3.3.1
# homeassistant.components.flick_electric
PyFlick==0.0.2
@@ -140,7 +140,7 @@ aiopylgtv==0.3.3
aiorecollect==1.0.1
# homeassistant.components.shelly
aioshelly==0.6.0
aioshelly==0.6.1
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -196,7 +196,7 @@ base36==0.1.1
bellows==0.21.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.14
bimmer_connected==0.7.15
# homeassistant.components.blebox
blebox_uniapi==1.3.2
@@ -367,7 +367,7 @@ google-api-python-client==1.6.4
google-cloud-pubsub==2.1.0
# homeassistant.components.nest
google-nest-sdm==0.2.10
google-nest-sdm==0.2.12
# homeassistant.components.gree
greeclimate==0.10.3
@@ -382,7 +382,7 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
ha-philipsjs==0.1.0
ha-philipsjs==2.3.0
# homeassistant.components.habitica
habitipy==0.2.0
@@ -412,7 +412,7 @@ hole==0.5.1
holidays==0.10.5.2
# homeassistant.components.frontend
home-assistant-frontend==20210224.0
home-assistant-frontend==20210226.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -688,7 +688,7 @@ pybotvac==0.0.20
pycfdns==1.2.1
# homeassistant.components.cast
pychromecast==8.1.0
pychromecast==8.1.2
# homeassistant.components.climacell
pyclimacell==0.14.0
@@ -1213,7 +1213,7 @@ zeep[async]==4.0.0
zeroconf==0.28.8
# homeassistant.components.zha
zha-quirks==0.0.53
zha-quirks==0.0.54
# homeassistant.components.zha
zigpy-cc==0.5.2
@@ -1234,4 +1234,4 @@ zigpy-znp==0.4.0
zigpy==0.32.0
# homeassistant.components.zwave_js
zwave-js-server-python==0.19.0
zwave-js-server-python==0.20.0
+1 -1
View File
@@ -32,7 +32,7 @@ PROJECT_URLS = {
PACKAGES = find_packages(exclude=["tests", "tests.*"])
REQUIRES = [
"aiohttp==3.7.3",
"aiohttp==3.7.4",
"astral==1.10.1",
"async_timeout==3.0.1",
"attrs==19.3.0",
+3 -7
View File
@@ -26,7 +26,7 @@ from homeassistant.components.media_player.const import (
import homeassistant.components.vacuum as vacuum
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import Context, callback
from homeassistant.core import Context
from homeassistant.helpers import entityfilter
from homeassistant.setup import async_setup_component
@@ -42,17 +42,13 @@ from . import (
reported_properties,
)
from tests.common import async_mock_service
from tests.common import async_capture_events, async_mock_service
@pytest.fixture
def events(hass):
"""Fixture that catches alexa events."""
events = []
hass.bus.async_listen(
smart_home.EVENT_ALEXA_SMART_HOME, callback(lambda e: events.append(e))
)
yield events
return async_capture_events(hass, smart_home.EVENT_ALEXA_SMART_HOME)
@pytest.fixture
+7 -5
View File
@@ -30,7 +30,12 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import assert_setup_component, async_mock_service, mock_restore_cache
from tests.common import (
assert_setup_component,
async_capture_events,
async_mock_service,
mock_restore_cache,
)
from tests.components.logbook.test_init import MockLazyEventPartialState
@@ -496,10 +501,7 @@ async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_onl
assert len(calls) == 1
assert calls[0].data.get("event") == "test_event"
test_reload_event = []
hass.bus.async_listen(
EVENT_AUTOMATION_RELOADED, lambda event: test_reload_event.append(event)
)
test_reload_event = async_capture_events(hass, EVENT_AUTOMATION_RELOADED)
with patch(
"homeassistant.config.load_yaml_config_file",
+2 -4
View File
@@ -12,7 +12,7 @@ from homeassistant.core import callback
from homeassistant.helpers import discovery
from homeassistant.setup import async_setup_component
from tests.common import assert_setup_component
from tests.common import assert_setup_component, async_capture_events
CONFIG = {notify.DOMAIN: {"platform": "demo"}}
@@ -20,9 +20,7 @@ CONFIG = {notify.DOMAIN: {"platform": "demo"}}
@pytest.fixture
def events(hass):
"""Fixture that catches notify events."""
events = []
hass.bus.async_listen(demo.EVENT_NOTIFY, callback(lambda e: events.append(e)))
yield events
return async_capture_events(hass, demo.EVENT_NOTIFY)
@pytest.fixture
@@ -30,7 +30,12 @@ from homeassistant.setup import async_setup_component
from . import BASIC_CONFIG, MockConfig
from tests.common import mock_area_registry, mock_device_registry, mock_registry
from tests.common import (
async_capture_events,
mock_area_registry,
mock_device_registry,
mock_registry,
)
REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf"
@@ -77,8 +82,7 @@ async def test_sync_message(hass):
},
)
events = []
hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append)
events = async_capture_events(hass, EVENT_SYNC_RECEIVED)
result = await sh.async_handle_message(
hass,
@@ -192,8 +196,7 @@ async def test_sync_in_area(area_on_device, hass, registries):
config = MockConfig(should_expose=lambda _: True, entity_config={})
events = []
hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append)
events = async_capture_events(hass, EVENT_SYNC_RECEIVED)
result = await sh.async_handle_message(
hass,
@@ -295,8 +298,7 @@ async def test_query_message(hass):
light3.entity_id = "light.color_temp_light"
await light3.async_update_ha_state()
events = []
hass.bus.async_listen(EVENT_QUERY_RECEIVED, events.append)
events = async_capture_events(hass, EVENT_QUERY_RECEIVED)
result = await sh.async_handle_message(
hass,
@@ -387,11 +389,8 @@ async def test_execute(hass):
"light", "turn_off", {"entity_id": "light.ceiling_lights"}, blocking=True
)
events = []
hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append)
service_events = []
hass.bus.async_listen(EVENT_CALL_SERVICE, service_events.append)
events = async_capture_events(hass, EVENT_COMMAND_RECEIVED)
service_events = async_capture_events(hass, EVENT_CALL_SERVICE)
result = await sh.async_handle_message(
hass,
@@ -570,8 +569,7 @@ async def test_raising_error_trait(hass):
{ATTR_MIN_TEMP: 15, ATTR_MAX_TEMP: 30, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
)
events = []
hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append)
events = async_capture_events(hass, EVENT_COMMAND_RECEIVED)
await hass.async_block_till_done()
result = await sh.async_handle_message(
@@ -660,8 +658,7 @@ async def test_unavailable_state_does_sync(hass):
light._available = False # pylint: disable=protected-access
await light.async_update_ha_state()
events = []
hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append)
events = async_capture_events(hass, EVENT_SYNC_RECEIVED)
result = await sh.async_handle_message(
hass,
@@ -54,7 +54,7 @@ from homeassistant.util import color
from . import BASIC_CONFIG, MockConfig
from tests.common import async_mock_service
from tests.common import async_capture_events, async_mock_service
REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf"
@@ -84,8 +84,7 @@ async def test_brightness_light(hass):
assert trt.query_attributes() == {"brightness": 95}
events = []
hass.bus.async_listen(EVENT_CALL_SERVICE, events.append)
events = async_capture_events(hass, EVENT_CALL_SERVICE)
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await trt.execute(
+2 -5
View File
@@ -8,17 +8,14 @@ from homeassistant.components.homeassistant import scene as ha_scene
from homeassistant.components.homeassistant.scene import EVENT_SCENE_RELOADED
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service
from tests.common import async_capture_events, async_mock_service
async def test_reload_config_service(hass):
"""Test the reload config service."""
assert await async_setup_component(hass, "scene", {})
test_reloaded_event = []
hass.bus.async_listen(
EVENT_SCENE_RELOADED, lambda event: test_reloaded_event.append(event)
)
test_reloaded_event = async_capture_events(hass, EVENT_SCENE_RELOADED)
with patch(
"homeassistant.config.load_yaml_config_file",
+3 -6
View File
@@ -5,7 +5,8 @@ from pyhap.accessory_driver import AccessoryDriver
import pytest
from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED
from homeassistant.core import callback as ha_callback
from tests.common import async_capture_events
@pytest.fixture
@@ -24,8 +25,4 @@ def hk_driver(loop):
@pytest.fixture
def events(hass):
"""Yield caught homekit_changed events."""
events = []
hass.bus.async_listen(
EVENT_HOMEKIT_CHANGED, ha_callback(lambda e: events.append(e))
)
yield events
return async_capture_events(hass, EVENT_HOMEKIT_CHANGED)
+1 -4
View File
@@ -175,10 +175,7 @@ async def test_cors_defaults(hass):
assert await async_setup_component(hass, "http", {})
assert len(mock_setup.mock_calls) == 1
assert mock_setup.mock_calls[0][1][1] == [
"https://cast.home-assistant.io",
"https://my.home-assistant.io",
]
assert mock_setup.mock_calls[0][1][1] == ["https://cast.home-assistant.io"]
async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port):
+17
View File
@@ -640,6 +640,15 @@ async def test_options_flow(hass):
assert result["type"] == "form"
assert result["step_id"] == "init"
schema = result["data_schema"].schema
assert (
_get_schema_default(schema, const.CONF_ALLOW_HUE_GROUPS)
== const.DEFAULT_ALLOW_HUE_GROUPS
)
assert (
_get_schema_default(schema, const.CONF_ALLOW_UNREACHABLE)
== const.DEFAULT_ALLOW_UNREACHABLE
)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
@@ -654,3 +663,11 @@ async def test_options_flow(hass):
const.CONF_ALLOW_HUE_GROUPS: True,
const.CONF_ALLOW_UNREACHABLE: True,
}
def _get_schema_default(schema, key_name):
"""Iterate schema to find a key."""
for schema_key in schema:
if schema_key == key_name:
return schema_key.default()
raise KeyError(f"{key_name} not found in schema")
+71 -28
View File
@@ -7,6 +7,12 @@ import aiohue
from homeassistant import config_entries
from homeassistant.components import hue
from homeassistant.components.hue import light as hue_light
from homeassistant.helpers.device_registry import (
async_get_registry as async_get_device_registry,
)
from homeassistant.helpers.entity_registry import (
async_get_registry as async_get_entity_registry,
)
from homeassistant.util import color
HUE_LIGHT_NS = "homeassistant.components.light.hue."
@@ -211,8 +217,10 @@ async def test_no_lights_or_groups(hass, mock_bridge):
async def test_lights(hass, mock_bridge):
"""Test the update_lights function with some lights."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 1
assert len(mock_bridge.mock_requests) == 2
# 2 lights
assert len(hass.states.async_all()) == 2
@@ -230,6 +238,8 @@ async def test_lights(hass, mock_bridge):
async def test_lights_color_mode(hass, mock_bridge):
"""Test that lights only report appropriate color mode."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
lamp_1 = hass.states.get("light.hue_lamp_1")
@@ -249,8 +259,8 @@ async def test_lights_color_mode(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_on", {"entity_id": "light.hue_lamp_2"}, blocking=True
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
# 2x light update, 1 group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
lamp_1 = hass.states.get("light.hue_lamp_1")
assert lamp_1 is not None
@@ -332,9 +342,10 @@ async def test_new_group_discovered(hass, mock_bridge):
async def test_new_light_discovered(hass, mock_bridge):
"""Test if 2nd update has a new light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 1
assert len(mock_bridge.mock_requests) == 2
assert len(hass.states.async_all()) == 2
new_light_response = dict(LIGHT_RESPONSE)
@@ -366,8 +377,8 @@ async def test_new_light_discovered(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
# 2x light update, 1 group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
assert len(hass.states.async_all()) == 3
light = hass.states.get("light.hue_lamp_3")
@@ -407,9 +418,10 @@ async def test_group_removed(hass, mock_bridge):
async def test_light_removed(hass, mock_bridge):
"""Test if 2nd update has removed light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 1
assert len(mock_bridge.mock_requests) == 2
assert len(hass.states.async_all()) == 2
mock_bridge.mock_light_responses.clear()
@@ -420,8 +432,8 @@ async def test_light_removed(hass, mock_bridge):
"light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
# 2x light update, 1 group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
assert len(hass.states.async_all()) == 1
light = hass.states.get("light.hue_lamp_1")
@@ -487,9 +499,10 @@ async def test_other_group_update(hass, mock_bridge):
async def test_other_light_update(hass, mock_bridge):
"""Test changing one light that will impact state of other light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 1
assert len(mock_bridge.mock_requests) == 2
assert len(hass.states.async_all()) == 2
lamp_2 = hass.states.get("light.hue_lamp_2")
@@ -526,8 +539,8 @@ async def test_other_light_update(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
# 2x light update, 1 group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
assert len(hass.states.async_all()) == 2
lamp_2 = hass.states.get("light.hue_lamp_2")
@@ -549,7 +562,6 @@ async def test_update_timeout(hass, mock_bridge):
async def test_update_unauthorized(hass, mock_bridge):
"""Test bridge marked as not authorized if unauthorized during update."""
mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized)
mock_bridge.api.groups.update = Mock(side_effect=aiohue.Unauthorized)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 0
assert len(hass.states.async_all()) == 0
@@ -559,6 +571,8 @@ async def test_update_unauthorized(hass, mock_bridge):
async def test_light_turn_on_service(hass, mock_bridge):
"""Test calling the turn on service on a light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
light = hass.states.get("light.hue_lamp_2")
assert light is not None
@@ -575,10 +589,10 @@ async def test_light_turn_on_service(hass, mock_bridge):
{"entity_id": "light.hue_lamp_2", "brightness": 100, "color_temp": 300},
blocking=True,
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
# 2x light update, 1 group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
assert mock_bridge.mock_requests[1]["json"] == {
assert mock_bridge.mock_requests[2]["json"] == {
"bri": 100,
"on": True,
"ct": 300,
@@ -599,9 +613,9 @@ async def test_light_turn_on_service(hass, mock_bridge):
blocking=True,
)
assert len(mock_bridge.mock_requests) == 5
assert len(mock_bridge.mock_requests) == 6
assert mock_bridge.mock_requests[3]["json"] == {
assert mock_bridge.mock_requests[4]["json"] == {
"on": True,
"xy": (0.138, 0.08),
"alert": "none",
@@ -611,6 +625,8 @@ async def test_light_turn_on_service(hass, mock_bridge):
async def test_light_turn_off_service(hass, mock_bridge):
"""Test calling the turn on service on a light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
light = hass.states.get("light.hue_lamp_1")
assert light is not None
@@ -624,10 +640,11 @@ async def test_light_turn_off_service(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_off", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
# 2x light update, 1 turn on request
assert len(mock_bridge.mock_requests) == 3
assert mock_bridge.mock_requests[1]["json"] == {"on": False, "alert": "none"}
# 2x light update, 1 for group update, 1 turn on request
assert len(mock_bridge.mock_requests) == 4
assert mock_bridge.mock_requests[2]["json"] == {"on": False, "alert": "none"}
assert len(hass.states.async_all()) == 2
@@ -649,6 +666,7 @@ def test_available():
bridge=Mock(allow_unreachable=False),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.available is False
@@ -664,6 +682,7 @@ def test_available():
bridge=Mock(allow_unreachable=True),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.available is True
@@ -679,6 +698,7 @@ def test_available():
bridge=Mock(allow_unreachable=False),
is_group=True,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.available is True
@@ -697,6 +717,7 @@ def test_hs_color():
bridge=Mock(),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.hs_color is None
@@ -712,6 +733,7 @@ def test_hs_color():
bridge=Mock(),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.hs_color is None
@@ -727,6 +749,7 @@ def test_hs_color():
bridge=Mock(),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
rooms={},
)
assert light.hs_color == color.color_xy_to_hs(0.4, 0.5, LIGHT_GAMUT)
@@ -742,7 +765,7 @@ async def test_group_features(hass, mock_bridge):
"1": {
"name": "Group 1",
"lights": ["1", "2"],
"type": "Room",
"type": "LightGroup",
"action": {
"on": True,
"bri": 254,
@@ -757,8 +780,8 @@ async def test_group_features(hass, mock_bridge):
"state": {"any_on": True, "all_on": False},
},
"2": {
"name": "Group 2",
"lights": ["3", "4"],
"name": "Living Room",
"lights": ["2", "3"],
"type": "Room",
"action": {
"on": True,
@@ -774,8 +797,8 @@ async def test_group_features(hass, mock_bridge):
"state": {"any_on": True, "all_on": False},
},
"3": {
"name": "Group 3",
"lights": ["1", "3"],
"name": "Dining Room",
"lights": ["4"],
"type": "Room",
"action": {
"on": True,
@@ -900,6 +923,7 @@ async def test_group_features(hass, mock_bridge):
mock_bridge.mock_light_responses.append(light_response)
mock_bridge.mock_group_responses.append(group_response)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 2
color_temp_feature = hue_light.SUPPORT_HUE["Color temperature light"]
extended_color_feature = hue_light.SUPPORT_HUE["Extended color light"]
@@ -907,8 +931,27 @@ async def test_group_features(hass, mock_bridge):
group_1 = hass.states.get("light.group_1")
assert group_1.attributes["supported_features"] == color_temp_feature
group_2 = hass.states.get("light.group_2")
group_2 = hass.states.get("light.living_room")
assert group_2.attributes["supported_features"] == extended_color_feature
group_3 = hass.states.get("light.group_3")
group_3 = hass.states.get("light.dining_room")
assert group_3.attributes["supported_features"] == extended_color_feature
entity_registry = await async_get_entity_registry(hass)
device_registry = await async_get_device_registry(hass)
entry = entity_registry.async_get("light.hue_lamp_1")
device_entry = device_registry.async_get(entry.device_id)
assert device_entry.suggested_area is None
entry = entity_registry.async_get("light.hue_lamp_2")
device_entry = device_registry.async_get(entry.device_id)
assert device_entry.suggested_area == "Living Room"
entry = entity_registry.async_get("light.hue_lamp_3")
device_entry = device_registry.async_get(entry.device_id)
assert device_entry.suggested_area == "Living Room"
entry = entity_registry.async_get("light.hue_lamp_4")
device_entry = device_registry.async_get(entry.device_id)
assert device_entry.suggested_area == "Dining Room"
@@ -290,6 +290,81 @@ async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock):
assert calls[1].data["some"] == "long_press"
async def test_if_fires_on_mqtt_message_template(hass, device_reg, calls, mqtt_mock):
"""Test triggers firing."""
data1 = (
'{ "automation_type":"trigger",'
' "device":{"identifiers":["0AFFD2"]},'
" \"payload\": \"{{ 'foo_press'|regex_replace('foo', 'short') }}\","
' "topic": "foobar/triggers/button{{ sqrt(16)|round }}",'
' "type": "button_short_press",'
' "subtype": "button_1",'
' "value_template": "{{ value_json.button }}"}'
)
data2 = (
'{ "automation_type":"trigger",'
' "device":{"identifiers":["0AFFD2"]},'
" \"payload\": \"{{ 'foo_press'|regex_replace('foo', 'long') }}\","
' "topic": "foobar/triggers/button{{ sqrt(16)|round }}",'
' "type": "button_long_press",'
' "subtype": "button_2",'
' "value_template": "{{ value_json.button }}"}'
)
async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2)
await hass.async_block_till_done()
device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")})
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": "device",
"domain": DOMAIN,
"device_id": device_entry.id,
"discovery_id": "bla1",
"type": "button_short_press",
"subtype": "button_1",
},
"action": {
"service": "test.automation",
"data_template": {"some": ("short_press")},
},
},
{
"trigger": {
"platform": "device",
"domain": DOMAIN,
"device_id": device_entry.id,
"discovery_id": "bla2",
"type": "button_1",
"subtype": "button_long_press",
},
"action": {
"service": "test.automation",
"data_template": {"some": ("long_press")},
},
},
]
},
)
# Fake short press.
async_fire_mqtt_message(hass, "foobar/triggers/button4", '{"button":"short_press"}')
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data["some"] == "short_press"
# Fake long press.
async_fire_mqtt_message(hass, "foobar/triggers/button4", '{"button":"long_press"}')
await hass.async_block_till_done()
assert len(calls) == 2
assert calls[1].data["some"] == "long_press"
async def test_if_fires_on_mqtt_message_late_discover(
hass, device_reg, calls, mqtt_mock
):
+25
View File
@@ -81,6 +81,31 @@ async def test_if_fires_on_topic_and_payload_match(hass, calls):
assert len(calls) == 1
async def test_if_fires_on_topic_and_payload_match2(hass, calls):
"""Test if message is fired on topic and payload match.
Make sure a payload which would render as a non string can still be matched.
"""
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "mqtt",
"topic": "test-topic",
"payload": "0",
},
"action": {"service": "test.automation"},
}
},
)
async_fire_mqtt_message(hass, "test-topic", "0")
await hass.async_block_till_done()
assert len(calls) == 1
async def test_if_fires_on_templated_topic_and_payload_match(hass, calls):
"""Test if message is fired on templated topic and payload match."""
assert await async_setup_component(
+56 -4
View File
@@ -3,6 +3,9 @@
MOCK_SERIAL_NO = "1234567890"
MOCK_NAME = "Philips TV"
MOCK_USERNAME = "mock_user"
MOCK_PASSWORD = "mock_password"
MOCK_SYSTEM = {
"menulanguage": "English",
"name": MOCK_NAME,
@@ -12,14 +15,63 @@ MOCK_SYSTEM = {
"model": "modelname",
}
MOCK_USERINPUT = {
"host": "1.1.1.1",
"api_version": 1,
MOCK_SYSTEM_UNPAIRED = {
"menulanguage": "Dutch",
"name": "55PUS7181/12",
"country": "Netherlands",
"serialnumber": "ABCDEFGHIJKLF",
"softwareversion": "TPM191E_R.101.001.208.001",
"model": "65OLED855/12",
"deviceid": "1234567890",
"nettvversion": "6.0.2",
"epgsource": "one",
"api_version": {"Major": 6, "Minor": 2, "Patch": 0},
"featuring": {
"jsonfeatures": {
"editfavorites": ["TVChannels", "SatChannels"],
"recordings": ["List", "Schedule", "Manage"],
"ambilight": ["LoungeLight", "Hue", "Ambilight"],
"menuitems": ["Setup_Menu"],
"textentry": [
"context_based",
"initial_string_available",
"editor_info_available",
],
"applications": ["TV_Apps", "TV_Games", "TV_Settings"],
"pointer": ["not_available"],
"inputkey": ["key"],
"activities": ["intent"],
"channels": ["preset_string"],
"mappings": ["server_mapping"],
},
"systemfeatures": {
"tvtype": "consumer",
"content": ["dmr", "dms_tad"],
"tvsearch": "intent",
"pairing_type": "digest_auth_pairing",
"secured_transport": "True",
},
},
}
MOCK_USERINPUT = {
"host": "1.1.1.1",
}
MOCK_IMPORT = {"host": "1.1.1.1", "api_version": 6}
MOCK_CONFIG = {
**MOCK_USERINPUT,
"host": "1.1.1.1",
"api_version": 1,
"system": MOCK_SYSTEM,
}
MOCK_CONFIG_PAIRED = {
"host": "1.1.1.1",
"api_version": 6,
"username": MOCK_USERNAME,
"password": MOCK_PASSWORD,
"system": MOCK_SYSTEM_UNPAIRED,
}
MOCK_ENTITY_ID = "media_player.philips_tv"
+11 -2
View File
@@ -1,6 +1,7 @@
"""Standard setup for tests."""
from unittest.mock import Mock, patch
from unittest.mock import create_autospec, patch
from haphilipsjs import PhilipsTV
from pytest import fixture
from homeassistant import setup
@@ -20,10 +21,18 @@ async def setup_notification(hass):
@fixture(autouse=True)
def mock_tv():
"""Disable component actual use."""
tv = Mock(autospec="philips_js.PhilipsTV")
tv = create_autospec(PhilipsTV)
tv.sources = {}
tv.channels = {}
tv.application = None
tv.applications = {}
tv.system = MOCK_SYSTEM
tv.api_version = 1
tv.api_version_detected = None
tv.on = True
tv.notify_change_supported = False
tv.pairing_type = None
tv.powerstate = None
with patch(
"homeassistant.components.philips_js.config_flow.PhilipsTV", return_value=tv
+140 -4
View File
@@ -1,12 +1,21 @@
"""Test the Philips TV config flow."""
from unittest.mock import patch
from unittest.mock import ANY, patch
from haphilipsjs import PairingFailure
from pytest import fixture
from homeassistant import config_entries
from homeassistant.components.philips_js.const import DOMAIN
from . import MOCK_CONFIG, MOCK_USERINPUT
from . import (
MOCK_CONFIG,
MOCK_CONFIG_PAIRED,
MOCK_IMPORT,
MOCK_PASSWORD,
MOCK_SYSTEM_UNPAIRED,
MOCK_USERINPUT,
MOCK_USERNAME,
)
@fixture(autouse=True)
@@ -27,12 +36,26 @@ def mock_setup_entry():
yield mock_setup_entry
@fixture
async def mock_tv_pairable(mock_tv):
"""Return a mock tv that is pariable."""
mock_tv.system = MOCK_SYSTEM_UNPAIRED
mock_tv.pairing_type = "digest_auth_pairing"
mock_tv.api_version = 6
mock_tv.api_version_detected = 6
mock_tv.secured_transport = True
mock_tv.pairRequest.return_value = {}
mock_tv.pairGrant.return_value = MOCK_USERNAME, MOCK_PASSWORD
return mock_tv
async def test_import(hass, mock_setup, mock_setup_entry):
"""Test we get an item on import."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=MOCK_USERINPUT,
data=MOCK_IMPORT,
)
assert result["type"] == "create_entry"
@@ -47,7 +70,7 @@ async def test_import_exist(hass, mock_config_entry):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=MOCK_USERINPUT,
data=MOCK_IMPORT,
)
assert result["type"] == "abort"
@@ -103,3 +126,116 @@ async def test_form_unexpected_error(hass, mock_tv):
assert result["type"] == "form"
assert result["errors"] == {"base": "unknown"}
async def test_pairing(hass, mock_tv_pairable, mock_setup, mock_setup_entry):
"""Test we get the form."""
mock_tv = mock_tv_pairable
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
MOCK_USERINPUT,
)
assert result["type"] == "form"
assert result["errors"] == {}
mock_tv.setTransport.assert_called_with(True)
mock_tv.pairRequest.assert_called()
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"pin": "1234"}
)
assert result == {
"flow_id": ANY,
"type": "create_entry",
"description": None,
"description_placeholders": None,
"handler": "philips_js",
"result": ANY,
"title": "55PUS7181/12 (ABCDEFGHIJKLF)",
"data": MOCK_CONFIG_PAIRED,
"version": 1,
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_pair_request_failed(
hass, mock_tv_pairable, mock_setup, mock_setup_entry
):
"""Test we get the form."""
mock_tv = mock_tv_pairable
mock_tv.pairRequest.side_effect = PairingFailure({})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
MOCK_USERINPUT,
)
assert result == {
"flow_id": ANY,
"description_placeholders": {"error_id": None},
"handler": "philips_js",
"reason": "pairing_failure",
"type": "abort",
}
async def test_pair_grant_failed(hass, mock_tv_pairable, mock_setup, mock_setup_entry):
"""Test we get the form."""
mock_tv = mock_tv_pairable
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
MOCK_USERINPUT,
)
assert result["type"] == "form"
assert result["errors"] == {}
mock_tv.setTransport.assert_called_with(True)
mock_tv.pairRequest.assert_called()
# Test with invalid pin
mock_tv.pairGrant.side_effect = PairingFailure({"error_id": "INVALID_PIN"})
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"pin": "1234"}
)
assert result["type"] == "form"
assert result["errors"] == {"pin": "invalid_pin"}
# Test with unexpected failure
mock_tv.pairGrant.side_effect = PairingFailure({})
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"pin": "1234"}
)
assert result == {
"flow_id": ANY,
"description_placeholders": {"error_id": None},
"handler": "philips_js",
"reason": "pairing_failure",
"type": "abort",
}
@@ -33,9 +33,13 @@ async def test_get_triggers(hass, mock_device):
assert_lists_same(triggers, expected_triggers)
async def test_if_fires_on_turn_on_request(hass, calls, mock_entity, mock_device):
async def test_if_fires_on_turn_on_request(
hass, calls, mock_tv, mock_entity, mock_device
):
"""Test for turn_on and turn_off triggers firing."""
mock_tv.on = False
assert await async_setup_component(
hass,
automation.DOMAIN,
+7 -5
View File
@@ -10,10 +10,14 @@ from homeassistant.components.shelly.const import (
DOMAIN,
EVENT_SHELLY_CLICK,
)
from homeassistant.core import callback as ha_callback
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_mock_service, mock_device_registry
from tests.common import (
MockConfigEntry,
async_capture_events,
async_mock_service,
mock_device_registry,
)
MOCK_SETTINGS = {
"name": "Test name",
@@ -81,9 +85,7 @@ def calls(hass):
@pytest.fixture
def events(hass):
"""Yield caught shelly_click events."""
ha_events = []
hass.bus.async_listen(EVENT_SHELLY_CLICK, ha_callback(ha_events.append))
yield ha_events
return async_capture_events(hass, EVENT_SHELLY_CLICK)
@pytest.fixture
@@ -144,6 +144,8 @@ def mock_get_server_version(server_version_side_effect, server_version_timeout):
driver_version="mock-driver-version",
server_version="mock-server-version",
home_id=1234,
min_schema_version=0,
max_schema_version=1,
)
with patch(
"homeassistant.components.zwave_js.config_flow.get_server_version",
+53
View File
@@ -124,6 +124,59 @@ async def test_on_node_added_ready(
)
async def test_unique_id_migration_dupes(
hass, multisensor_6_state, client, integration
):
"""Test we remove an entity when ."""
ent_reg = entity_registry.async_get(hass)
entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1]
# Create entity RegistryEntry using old unique ID format
old_unique_id_1 = (
f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00"
)
entity_entry = ent_reg.async_get_or_create(
"sensor",
DOMAIN,
old_unique_id_1,
suggested_object_id=entity_name,
config_entry=integration,
original_name=entity_name,
)
assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR
assert entity_entry.unique_id == old_unique_id_1
# Create entity RegistryEntry using b0 unique ID format
old_unique_id_2 = (
f"{client.driver.controller.home_id}.52.52-49-0-Air temperature-00-00"
)
entity_entry = ent_reg.async_get_or_create(
"sensor",
DOMAIN,
old_unique_id_2,
suggested_object_id=f"{entity_name}_1",
config_entry=integration,
original_name=entity_name,
)
assert entity_entry.entity_id == f"{AIR_TEMPERATURE_SENSOR}_1"
assert entity_entry.unique_id == old_unique_id_2
# Add a ready node, unique ID should be migrated
node = Node(client, multisensor_6_state)
event = {"node": node}
client.driver.controller.emit("node added", event)
await hass.async_block_till_done()
# Check that new RegistryEntry is using new unique ID format
entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature-00-00"
assert entity_entry.unique_id == new_unique_id
assert ent_reg.async_get(f"{AIR_TEMPERATURE_SENSOR}_1") is None
async def test_unique_id_migration_v1(hass, multisensor_6_state, client, integration):
"""Test unique ID is migrated from old format to new (version 1)."""
ent_reg = entity_registry.async_get(hass)