Merge pull request #33985 from home-assistant/rc

0.108.3
This commit is contained in:
Paulus Schoutsen 2020-04-10 15:24:17 -07:00 committed by GitHub
commit 10799952af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 173 additions and 67 deletions

View File

@ -1,11 +1,15 @@
ARG BUILD_FROM
FROM ${BUILD_FROM}
ENV \
S6_SERVICES_GRACETIME=60000
WORKDIR /usr/src
## Setup Home Assistant
COPY . homeassistant/
RUN pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
RUN \
pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
-r homeassistant/requirements_all.txt -c homeassistant/homeassistant/package_constraints.txt \
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
-e ./homeassistant \

View File

@ -6,6 +6,7 @@ from zlib import adler32
import voluptuous as vol
from homeassistant.components import cover
from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE
from homeassistant.components.media_player import DEVICE_CLASS_TV
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@ -200,7 +201,7 @@ def get_accessory(hass, driver, state, aid, config):
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if device_class == "garage" and features & (
if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & (
cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE
):
a_type = "GarageDoorOpener"

View File

@ -2,6 +2,6 @@
"domain": "homekit",
"name": "HomeKit",
"documentation": "https://www.home-assistant.io/integrations/homekit",
"requirements": ["HAP-python==2.8.1"],
"requirements": ["HAP-python==2.8.2"],
"codeowners": []
}

View File

@ -116,7 +116,8 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("IPP Parse Error")
return self.async_abort(reason="parse_error")
self.discovery_info[CONF_UUID] = info[CONF_UUID]
if info[CONF_UUID] is not None:
self.discovery_info[CONF_UUID] = info[CONF_UUID]
await self.async_set_unique_id(self.discovery_info[CONF_UUID])
self._abort_if_unique_id_configured(

View File

@ -2,7 +2,7 @@
"domain": "ipp",
"name": "Internet Printing Protocol (IPP)",
"documentation": "https://www.home-assistant.io/integrations/ipp",
"requirements": ["pyipp==0.9.1"],
"requirements": ["pyipp==0.9.2"],
"codeowners": ["@ctalkington"],
"config_flow": true,
"quality_scale": "platinum",

View File

@ -1,7 +1,7 @@
{
"domain": "nexia",
"name": "Nexia",
"requirements": ["nexia==0.8.0"],
"requirements": ["nexia==0.8.2"],
"codeowners": ["@ryannazaretian", "@bdraco"],
"documentation": "https://www.home-assistant.io/integrations/nexia",
"config_flow": true

View File

@ -411,8 +411,11 @@ class ONVIFHassCamera(Camera):
req = media_service.create_type("GetSnapshotUri")
req.ProfileToken = profiles[self._profile_index].token
snapshot_uri = await media_service.GetSnapshotUri(req)
self._snapshot = snapshot_uri.Uri
try:
snapshot_uri = await media_service.GetSnapshotUri(req)
self._snapshot = snapshot_uri.Uri
except ServerDisconnectedError as err:
_LOGGER.debug("Camera does not support GetSnapshotUri: %s", err)
_LOGGER.debug(
"ONVIF Camera Using the following URL for %s snapshot: %s",

View File

@ -129,7 +129,7 @@ class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorDevice):
@property
def is_on(self):
"""Get the current value in kWh."""
"""Grid is online."""
return (
self._coordinator.data[POWERWALL_API_GRID_STATUS] == POWERWALL_GRID_ONLINE
)

View File

@ -2,12 +2,10 @@
DOMAIN = "powerwall"
POWERWALL_SITE_NAME = "site_name"
POWERWALL_OBJECT = "powerwall"
POWERWALL_COORDINATOR = "coordinator"
UPDATE_INTERVAL = 60
UPDATE_INTERVAL = 30
ATTR_REGION = "region"
ATTR_GRID_CODE = "grid_code"
@ -46,3 +44,5 @@ POWERWALL_RUNNING_KEY = "running"
MODEL = "PowerWall 2"
MANUFACTURER = "Tesla"
ENERGY_KILO_WATT = "kW"

View File

@ -4,7 +4,6 @@ import logging
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR,
UNIT_PERCENTAGE,
)
@ -14,6 +13,7 @@ from .const import (
ATTR_FREQUENCY,
ATTR_INSTANT_AVERAGE_VOLTAGE,
DOMAIN,
ENERGY_KILO_WATT,
POWERWALL_API_CHARGE,
POWERWALL_API_DEVICE_TYPE,
POWERWALL_API_METERS,
@ -87,7 +87,7 @@ class PowerWallEnergySensor(PowerWallEntity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return ENERGY_KILO_WATT_HOUR
return ENERGY_KILO_WATT
@property
def name(self):
@ -106,7 +106,7 @@ class PowerWallEnergySensor(PowerWallEntity):
@property
def state(self):
"""Get the current value in kWh."""
"""Get the current value in kW."""
meter = self._coordinator.data[POWERWALL_API_METERS][self._meter]
return round(float(meter.instant_power / 1000), 3)

View File

@ -32,6 +32,7 @@ from .core.const import (
SIGNAL_ADD_ENTITIES,
RadioType,
)
from .core.discovery import GROUP_PROBE
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(ha_const.CONF_TYPE): cv.string})
@ -138,6 +139,7 @@ async def async_unload_entry(hass, config_entry):
"""Unload ZHA config entry."""
await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].shutdown()
GROUP_PROBE.cleanup()
api.async_unload_api(hass)
dispatchers = hass.data[DATA_ZHA].get(DATA_ZHA_DISPATCHERS, [])

View File

@ -208,6 +208,7 @@ SIGNAL_SET_LEVEL = "set_level"
SIGNAL_STATE_ATTR = "update_state_attribute"
SIGNAL_UPDATE_DEVICE = "{}_zha_update_device"
SIGNAL_REMOVE_GROUP = "remove_group"
SIGNAL_GROUP_ENTITY_REMOVED = "group_entity_removed"
SIGNAL_GROUP_MEMBERSHIP_CHANGE = "group_membership_change"
UNKNOWN = "unknown"

View File

@ -551,7 +551,15 @@ class ZHADevice(LogMixin):
async def async_remove_from_group(self, group_id):
"""Remove this device from the provided zigbee group."""
await self._zigpy_device.remove_from_group(group_id)
try:
await self._zigpy_device.remove_from_group(group_id)
except (zigpy.exceptions.DeliveryError, asyncio.TimeoutError) as ex:
self.debug(
"Failed to remove device '%s' from group: 0x%04x ex: %s",
self._zigpy_device.ieee,
group_id,
str(ex),
)
async def async_bind_to_group(self, group_id, cluster_bindings):
"""Directly bind this device to a group for the given clusters."""

View File

@ -6,7 +6,10 @@ from typing import Callable, List, Tuple
from homeassistant import const as ha_const
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.helpers.typing import HomeAssistantType
@ -166,10 +169,30 @@ class GroupProbe:
def __init__(self):
"""Initialize instance."""
self._hass = None
self._unsubs = []
def initialize(self, hass: HomeAssistantType) -> None:
"""Initialize the group probe."""
self._hass = hass
self._unsubs.append(
async_dispatcher_connect(
hass, zha_const.SIGNAL_GROUP_ENTITY_REMOVED, self._reprobe_group
)
)
def cleanup(self):
"""Clean up on when zha shuts down."""
for unsub in self._unsubs[:]:
unsub()
self._unsubs.remove(unsub)
def _reprobe_group(self, group_id: int) -> None:
"""Reprobe a group for entities after its members change."""
zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]
zha_group = zha_gateway.groups.get(group_id)
if zha_group is None:
return
self.discover_group_entities(zha_group)
@callback
def discover_group_entities(self, group: zha_typing.ZhaGroupType) -> None:

View File

@ -20,7 +20,10 @@ from homeassistant.helpers.device_registry import (
async_get_registry as get_dev_reg,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg
from homeassistant.helpers.entity_registry import (
async_entries_for_device,
async_get_registry as get_ent_reg,
)
from . import discovery, typing as zha_typing
from .const import (
@ -77,7 +80,7 @@ from .const import (
from .device import DeviceStatus, ZHADevice
from .group import ZHAGroup
from .patches import apply_application_controller_patch
from .registries import RADIO_TYPES
from .registries import GROUP_ENTITY_DOMAINS, RADIO_TYPES
from .store import async_get_registry
from .typing import ZhaDeviceType, ZhaGroupType, ZigpyEndpointType, ZigpyGroupType
@ -273,6 +276,9 @@ class ZHAGateway:
async_dispatcher_send(
self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{zigpy_group.group_id:04x}"
)
if len(zha_group.members) == 2:
# we need to do this because there wasn't already a group entity to remove and re-add
discovery.GROUP_PROBE.discover_group_entities(zha_group)
def group_added(self, zigpy_group: ZigpyGroupType) -> None:
"""Handle zigpy group added event."""
@ -289,6 +295,7 @@ class ZHAGateway:
async_dispatcher_send(
self._hass, f"{SIGNAL_REMOVE_GROUP}_0x{zigpy_group.group_id:04x}"
)
self._cleanup_group_entity_registry_entries(zigpy_group)
def _send_group_gateway_message(
self, zigpy_group: ZigpyGroupType, gateway_message_type: str
@ -368,6 +375,35 @@ class ZHAGateway:
e for e in entity_refs if e.reference_id != entity.entity_id
]
def _cleanup_group_entity_registry_entries(
self, zigpy_group: ZigpyGroupType
) -> None:
"""Remove entity registry entries for group entities when the groups are removed from HA."""
# first we collect the potential unique ids for entities that could be created from this group
possible_entity_unique_ids = [
f"{domain}_zha_group_0x{zigpy_group.group_id:04x}"
for domain in GROUP_ENTITY_DOMAINS
]
# then we get all group entity entries tied to the coordinator
all_group_entity_entries = async_entries_for_device(
self.ha_entity_registry, self.coordinator_zha_device.device_id
)
# then we get the entity entries for this specific group by getting the entries that match
entries_to_remove = [
entry
for entry in all_group_entity_entries
if entry.unique_id in possible_entity_unique_ids
]
# then we remove the entries from the entity registry
for entry in entries_to_remove:
_LOGGER.debug(
"cleaning up entity registry entry for entity: %s", entry.entity_id
)
self.ha_entity_registry.async_remove(entry.entity_id)
@property
def devices(self):
"""Return devices."""
@ -557,15 +593,7 @@ class ZHAGateway:
)
tasks.append(self.devices[ieee].async_add_to_group(group_id))
await asyncio.gather(*tasks)
zha_group = self.groups.get(group_id)
_LOGGER.debug(
"Probing group: %s:0x%04x for entity discovery",
zha_group.name,
zha_group.group_id,
)
discovery.GROUP_PROBE.discover_group_entities(zha_group)
return zha_group
return self.groups.get(group_id)
async def async_remove_zigpy_group(self, group_id: int) -> None:
"""Remove a Zigbee group from Zigpy."""

View File

@ -8,7 +8,10 @@ from typing import Any, Awaitable, Dict, List, Optional
from homeassistant.core import CALLBACK_TYPE, State, callback
from homeassistant.helpers import entity
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.restore_state import RestoreEntity
@ -19,6 +22,7 @@ from .core.const import (
DATA_ZHA,
DATA_ZHA_BRIDGE_ID,
DOMAIN,
SIGNAL_GROUP_ENTITY_REMOVED,
SIGNAL_GROUP_MEMBERSHIP_CHANGE,
SIGNAL_REMOVE,
SIGNAL_REMOVE_GROUP,
@ -32,7 +36,7 @@ ENTITY_SUFFIX = "entity_suffix"
RESTART_GRACE_PERIOD = 7200 # 2 hours
class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity):
class BaseZhaEntity(LogMixin, entity.Entity):
"""A base class for ZHA entities."""
def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs):
@ -112,7 +116,6 @@ class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity):
@callback
def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None:
"""Set the entity state."""
pass
async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass."""
@ -133,11 +136,6 @@ class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity):
self.zha_device.gateway.remove_entity_reference(self)
self.remove_future.set_result(True)
@callback
def async_restore_last_state(self, last_state) -> None:
"""Restore previous state."""
pass
async def async_accept_signal(
self, channel: ChannelType, signal: str, func: CALLABLE_T, signal_override=False
):
@ -158,7 +156,7 @@ class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity):
_LOGGER.log(level, msg, *args)
class ZhaEntity(BaseZhaEntity):
class ZhaEntity(BaseZhaEntity, RestoreEntity):
"""A base class for non group ZHA entities."""
def __init__(
@ -181,6 +179,13 @@ class ZhaEntity(BaseZhaEntity):
async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass."""
await super().async_added_to_hass()
self.remove_future = asyncio.Future()
await self.async_accept_signal(
None,
f"{SIGNAL_REMOVE}_{self.zha_device.ieee}",
self.async_remove,
signal_override=True,
)
await self.async_check_recently_seen()
await self.async_accept_signal(
None,
@ -197,6 +202,16 @@ class ZhaEntity(BaseZhaEntity):
self.remove_future,
)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect entity object when removed."""
await super().async_will_remove_from_hass()
self.zha_device.gateway.remove_entity_reference(self)
self.remove_future.set_result(True)
@callback
def async_restore_last_state(self, last_state) -> None:
"""Restore previous state."""
async def async_check_recently_seen(self) -> None:
"""Check if the device was seen within the last 2 hours."""
last_state = await self.async_get_last_state()
@ -246,13 +261,20 @@ class ZhaGroupEntity(BaseZhaEntity):
await self.async_accept_signal(
None,
f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{self._group_id:04x}",
self._update_group_entities,
self.async_remove,
signal_override=True,
)
self._async_unsub_state_changed = async_track_state_change(
self.hass, self._entity_ids, self.async_state_changed_listener
)
def send_removed_signal():
async_dispatcher_send(
self.hass, SIGNAL_GROUP_ENTITY_REMOVED, self._group_id
)
self.async_on_remove(send_removed_signal)
await self.async_update()
@callback
@ -262,17 +284,6 @@ class ZhaGroupEntity(BaseZhaEntity):
"""Handle child updates."""
self.async_schedule_update_ha_state(True)
def _update_group_entities(self):
"""Update tracked entities when membership changes."""
group = self.zha_device.gateway.get_group(self._group_id)
self._entity_ids = group.get_domain_entity_ids(self.platform.domain)
if self._async_unsub_state_changed is not None:
self._async_unsub_state_changed()
self._async_unsub_state_changed = async_track_state_change(
self.hass, self._entity_ids, self.async_state_changed_listener
)
async def async_will_remove_from_hass(self) -> None:
"""Handle removal from Home Assistant."""
await super().async_will_remove_from_hass()

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 108
PATCH_VERSION = "2"
PATCH_VERSION = "3"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 0)

View File

@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2
# Adafruit_BBIO==1.1.1
# homeassistant.components.homekit
HAP-python==2.8.1
HAP-python==2.8.2
# homeassistant.components.mastodon
Mastodon.py==1.5.0
@ -922,7 +922,7 @@ netdisco==2.6.0
neurio==0.3.1
# homeassistant.components.nexia
nexia==0.8.0
nexia==0.8.2
# homeassistant.components.nextcloud
nextcloudmonitor==1.1.0
@ -1336,7 +1336,7 @@ pyintesishome==1.7.1
pyipma==2.0.5
# homeassistant.components.ipp
pyipp==0.9.1
pyipp==0.9.2
# homeassistant.components.iqvia
pyiqvia==0.2.1

View File

@ -4,7 +4,7 @@
-r requirements_test.txt
# homeassistant.components.homekit
HAP-python==2.8.1
HAP-python==2.8.2
# homeassistant.components.mobile_app
# homeassistant.components.owntracks
@ -357,7 +357,7 @@ nessclient==0.9.15
netdisco==2.6.0
# homeassistant.components.nexia
nexia==0.8.0
nexia==0.8.2
# homeassistant.components.nsw_fuel_station
nsw-fuel-api-client==1.0.10
@ -519,7 +519,7 @@ pyicloud==0.9.6.1
pyipma==2.0.5
# homeassistant.components.ipp
pyipp==0.9.1
pyipp==0.9.2
# homeassistant.components.iqvia
pyiqvia==0.2.1

View File

@ -1,7 +1,8 @@
#!/usr/bin/execlineb -S0
#!/usr/bin/execlineb -S1
# ==============================================================================
# Take down the S6 supervision tree when Home Assistant fails
# ==============================================================================
if { s6-test ${1} -ne 100 }
if { s6-test ${1} -ne 256 }
s6-svscanctl -t /var/run/s6/services
s6-svscanctl -t /var/run/s6/services

View File

@ -39,13 +39,14 @@ async def test_sensors(hass):
"energy_exported": 10429451.9916853,
"energy_imported": 4824191.60668611,
"instant_average_voltage": 120.650001525879,
"unit_of_measurement": "kWh",
"unit_of_measurement": "kW",
"friendly_name": "Powerwall Site Now",
"device_class": "power",
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
assert all(item in state.attributes.items() for item in expected_attributes.items())
for key, value in expected_attributes.items():
assert state.attributes[key] == value
state = hass.states.get("sensor.powerwall_load_now")
assert state.state == "1.971"
@ -54,13 +55,14 @@ async def test_sensors(hass):
"energy_exported": 1056797.48917483,
"energy_imported": 4692987.91889705,
"instant_average_voltage": 120.650001525879,
"unit_of_measurement": "kWh",
"unit_of_measurement": "kW",
"friendly_name": "Powerwall Load Now",
"device_class": "power",
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
assert all(item in state.attributes.items() for item in expected_attributes.items())
for key, value in expected_attributes.items():
assert state.attributes[key] == value
state = hass.states.get("sensor.powerwall_battery_now")
assert state.state == "-8.55"
@ -69,13 +71,14 @@ async def test_sensors(hass):
"energy_exported": 3620010,
"energy_imported": 4216170,
"instant_average_voltage": 240.56,
"unit_of_measurement": "kWh",
"unit_of_measurement": "kW",
"friendly_name": "Powerwall Battery Now",
"device_class": "power",
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
assert all(item in state.attributes.items() for item in expected_attributes.items())
for key, value in expected_attributes.items():
assert state.attributes[key] == value
state = hass.states.get("sensor.powerwall_solar_now")
assert state.state == "10.49"
@ -84,13 +87,14 @@ async def test_sensors(hass):
"energy_exported": 9864205.82222448,
"energy_imported": 28177.5358355867,
"instant_average_voltage": 120.685001373291,
"unit_of_measurement": "kWh",
"unit_of_measurement": "kW",
"friendly_name": "Powerwall Solar Now",
"device_class": "power",
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
assert all(item in state.attributes.items() for item in expected_attributes.items())
for key, value in expected_attributes.items():
assert state.attributes[key] == value
state = hass.states.get("sensor.powerwall_charge")
assert state.state == "47.32"
@ -101,4 +105,5 @@ async def test_sensors(hass):
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
assert all(item in state.attributes.items() for item in expected_attributes.items())
for key, value in expected_attributes.items():
assert state.attributes[key] == value

View File

@ -539,3 +539,21 @@ async def async_test_zha_group_light_entity(
await zha_group.async_add_members([device_light_3.ieee])
await dev3_cluster_on_off.on()
assert hass.states.get(entity_id).state == STATE_ON
# make the group have only 1 member and now there should be no entity
await zha_group.async_remove_members([device_light_2.ieee, device_light_3.ieee])
assert len(zha_group.members) == 1
assert hass.states.get(entity_id).state is None
# make sure the entity registry entry is still there
assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None
# add a member back and ensure that the group entity was created again
await zha_group.async_add_members([device_light_3.ieee])
await dev3_cluster_on_off.on()
assert hass.states.get(entity_id).state == STATE_ON
# remove the group and ensure that there is no entity and that the entity registry is cleaned up
assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None
await zha_gateway.async_remove_zigpy_group(zha_group.group_id)
assert hass.states.get(entity_id).state is None
assert zha_gateway.ha_entity_registry.async_get(entity_id) is None