Skip to content

Commit

Permalink
Refactor of Hue integration with full V2 support (home-assistant#58996)
Browse files Browse the repository at this point in the history
Co-authored-by: Paulus Schoutsen <[email protected]>
  • Loading branch information
marcelveldt and balloob authored Nov 16, 2021
1 parent 4642a70 commit e1e6925
Show file tree
Hide file tree
Showing 55 changed files with 7,163 additions and 2,272 deletions.
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ homeassistant/components/homematic/* @pvizeli @danielperna84
homeassistant/components/honeywell/* @rdfurman
homeassistant/components/http/* @home-assistant/core
homeassistant/components/huawei_lte/* @scop @fphammerle
homeassistant/components/hue/* @balloob @frenck
homeassistant/components/hue/* @balloob @frenck @marcelveldt
homeassistant/components/huisbaasje/* @dennisschroer
homeassistant/components/humidifier/* @home-assistant/core @Shulyaka
homeassistant/components/hunterdouglas_powerview/* @bdraco
Expand Down
179 changes: 53 additions & 126 deletions homeassistant/components/hue/__init__.py
Original file line number Diff line number Diff line change
@@ -1,79 +1,41 @@
"""Support for the Philips Hue system."""
import asyncio
import logging

from aiohue.util import normalize_bridge_id
import voluptuous as vol

from homeassistant import config_entries, core
from homeassistant.components import persistent_notification
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.service import verify_domain_control
from homeassistant.helpers import device_registry as dr

from .bridge import HueBridge
from .const import (
ATTR_GROUP_NAME,
ATTR_SCENE_NAME,
ATTR_TRANSITION,
CONF_ALLOW_HUE_GROUPS,
CONF_ALLOW_UNREACHABLE,
DEFAULT_ALLOW_HUE_GROUPS,
DEFAULT_ALLOW_UNREACHABLE,
DOMAIN,
)

_LOGGER = logging.getLogger(__name__)
SERVICE_HUE_SCENE = "hue_activate_scene"
from .const import DOMAIN, SERVICE_HUE_ACTIVATE_SCENE
from .migration import check_migration
from .services import async_register_services


async def async_setup_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
) -> bool:
"""Set up a bridge from a config entry."""
# check (and run) migrations if needed
await check_migration(hass, entry)

# Migrate allow_unreachable from config entry data to config entry options
if (
CONF_ALLOW_UNREACHABLE not in entry.options
and CONF_ALLOW_UNREACHABLE in entry.data
and entry.data[CONF_ALLOW_UNREACHABLE] != DEFAULT_ALLOW_UNREACHABLE
):
options = {
**entry.options,
CONF_ALLOW_UNREACHABLE: entry.data[CONF_ALLOW_UNREACHABLE],
}
data = entry.data.copy()
data.pop(CONF_ALLOW_UNREACHABLE)
hass.config_entries.async_update_entry(entry, data=data, options=options)

# Migrate allow_hue_groups from config entry data to config entry options
if (
CONF_ALLOW_HUE_GROUPS not in entry.options
and CONF_ALLOW_HUE_GROUPS in entry.data
and entry.data[CONF_ALLOW_HUE_GROUPS] != DEFAULT_ALLOW_HUE_GROUPS
):
options = {
**entry.options,
CONF_ALLOW_HUE_GROUPS: entry.data[CONF_ALLOW_HUE_GROUPS],
}
data = entry.data.copy()
data.pop(CONF_ALLOW_HUE_GROUPS)
hass.config_entries.async_update_entry(entry, data=data, options=options)

# setup the bridge instance
bridge = HueBridge(hass, entry)

if not await bridge.async_setup():
if not await bridge.async_initialize_bridge():
return False

_register_services(hass)
# register Hue domain services
async_register_services(hass)

config = bridge.api.config
api = bridge.api

# For backwards compat
unique_id = normalize_bridge_id(config.bridgeid)
unique_id = normalize_bridge_id(api.config.bridge_id)
if entry.unique_id is None:
hass.config_entries.async_update_entry(entry, unique_id=unique_id)

# For recovering from bug where we incorrectly assumed homekit ID = bridge ID
# Remove this logic after Home Assistant 2022.4
elif entry.unique_id != unique_id:
# Find entries with this unique ID
other_entry = next(
Expand All @@ -84,7 +46,6 @@ async def async_setup_entry(
),
None,
)

if other_entry is None:
# If no other entry, update unique ID of this entry ID.
hass.config_entries.async_update_entry(entry, unique_id=unique_id)
Expand All @@ -100,88 +61,54 @@ async def async_setup_entry(
hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
return False

# add bridge device to device registry
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, config.mac)},
identifiers={(DOMAIN, config.bridgeid)},
manufacturer="Signify",
name=config.name,
model=config.modelid,
sw_version=config.swversion,
)

if config.modelid == "BSB002" and config.swversion < "1935144040":
persistent_notification.async_create(
hass,
"Your Hue hub has a known security vulnerability ([CVE-2020-6007](https://cve.circl.lu/cve/CVE-2020-6007)). Go to the Hue app and check for software updates.",
"Signify Hue",
"hue_hub_firmware",
if bridge.api_version == 1:
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, api.config.mac_address)},
identifiers={(DOMAIN, api.config.bridge_id)},
manufacturer="Signify",
name=api.config.name,
model=api.config.model_id,
sw_version=api.config.software_version,
)

elif config.swupdate2_bridge_state == "readytoinstall":
err = (
"Please check for software updates of the bridge in the Philips Hue App.",
"Signify Hue",
"hue_hub_firmware",
# create persistent notification if we found a bridge version with security vulnerability
if (
api.config.model_id == "BSB002"
and api.config.software_version < "1935144040"
):
persistent_notification.async_create(
hass,
"Your Hue hub has a known security vulnerability ([CVE-2020-6007] "
"(https://cve.circl.lu/cve/CVE-2020-6007)). "
"Go to the Hue app and check for software updates.",
"Signify Hue",
"hue_hub_firmware",
)
else:
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, api.config.mac_address)},
identifiers={
(DOMAIN, api.config.bridge_id),
(DOMAIN, api.config.bridge_device.id),
},
manufacturer=api.config.bridge_device.product_data.manufacturer_name,
name=api.config.name,
model=api.config.model_id,
sw_version=api.config.software_version,
)
_LOGGER.warning(err)

return True


async def async_unload_entry(hass, entry):
async def async_unload_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Unload a config entry."""
unload_success = await hass.data[DOMAIN][entry.entry_id].async_reset()
if len(hass.data[DOMAIN]) == 0:
hass.data.pop(DOMAIN)
hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE)
hass.services.async_remove(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE)
return unload_success


@core.callback
def _register_services(hass):
"""Register Hue services."""

async def hue_activate_scene(call, skip_reload=True):
"""Handle activation of Hue scene."""
# Get parameters
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]

# Call the set scene function on each bridge
tasks = [
bridge.hue_activate_scene(
call.data, skip_reload=skip_reload, hide_warnings=skip_reload
)
for bridge in hass.data[DOMAIN].values()
if isinstance(bridge, HueBridge)
]
results = await asyncio.gather(*tasks)

# Did *any* bridge succeed? If not, refresh / retry
# Note that we'll get a "None" value for a successful call
if None not in results:
if skip_reload:
await hue_activate_scene(call, skip_reload=False)
return
_LOGGER.warning(
"No bridge was able to activate " "scene %s in group %s",
scene_name,
group_name,
)

if not hass.services.has_service(DOMAIN, SERVICE_HUE_SCENE):
# Register a local handler for scene activation
hass.services.async_register(
DOMAIN,
SERVICE_HUE_SCENE,
verify_domain_control(hass, DOMAIN)(hue_activate_scene),
schema=vol.Schema(
{
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
vol.Optional(ATTR_TRANSITION): cv.positive_int,
}
),
)
80 changes: 24 additions & 56 deletions homeassistant/components/hue/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,24 @@
"""Hue binary sensor entities."""
from aiohue.sensors import TYPE_ZLL_PRESENCE

from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOTION,
BinarySensorEntity,
)

from .const import DOMAIN as HUE_DOMAIN
from .sensor_base import SENSOR_CONFIG_MAP, GenericZLLSensor

PRESENCE_NAME_FORMAT = "{} motion"


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Defer binary sensor setup to the shared sensor module."""
bridge = hass.data[HUE_DOMAIN][config_entry.entry_id]

if not bridge.sensor_manager:
return

await bridge.sensor_manager.async_register_component(
"binary_sensor", async_add_entities
)


class HuePresence(GenericZLLSensor, BinarySensorEntity):
"""The presence sensor entity for a Hue motion sensor device."""

_attr_device_class = DEVICE_CLASS_MOTION

@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.sensor.presence

@property
def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = super().extra_state_attributes
if "sensitivity" in self.sensor.config:
attributes["sensitivity"] = self.sensor.config["sensitivity"]
if "sensitivitymax" in self.sensor.config:
attributes["sensitivity_max"] = self.sensor.config["sensitivitymax"]
return attributes


SENSOR_CONFIG_MAP.update(
{
TYPE_ZLL_PRESENCE: {
"platform": "binary_sensor",
"name_format": PRESENCE_NAME_FORMAT,
"class": HuePresence,
}
}
)
"""Support for Hue binary sensors."""
from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .bridge import HueBridge
from .const import DOMAIN
from .v1.binary_sensor import async_setup_entry as setup_entry_v1
from .v2.binary_sensor import async_setup_entry as setup_entry_v2


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up binary sensor entities."""
bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id]
if bridge.api_version == 1:
await setup_entry_v1(hass, config_entry, async_add_entities)
else:
await setup_entry_v2(hass, config_entry, async_add_entities)
Loading

0 comments on commit e1e6925

Please sign in to comment.