Compare commits

..

34 Commits

Author SHA1 Message Date
Paulus Schoutsen
607b09ccdd Merge pull request #34260 from home-assistant/rc
0.108.5
2020-04-15 09:45:10 -07:00
Paulus Schoutsen
76b65c5779 Bumped version to 0.108.5 2020-04-15 08:46:21 -07:00
Fredrik Erlandsson
632d44c7b7 Fix various Daikin issues (#34249)
* various Daikin fixes

* make timeout a constant
2020-04-15 08:45:34 -07:00
Fredrik Erlandsson
1b36a34ae4 Add daikin update_before_add (#34248) 2020-04-15 08:45:07 -07:00
Paulus Schoutsen
0afb849e7f Fix Cloud UI bug preventing managing Google 2FA (#34241)
* Fix Cloud UI bug preventing managing Google 2FA

* Update comment
2020-04-15 08:45:06 -07:00
Aaron Bach
5f97937ba0 Fix websocket connection bug/errant logic in Ambient PWS (#34217) 2020-04-15 08:45:05 -07:00
Chris Talkington
ca48148150 Catch IPPVersionNotSupportedError in IPP (#34184)
* Update config_flow.py

* squash.

* Update test_config_flow.py

* Update config_flow.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update test_config_flow.py
2020-04-15 08:45:04 -07:00
Franck Nijhof
9142fa1aa6 Temporary transition Docker init (#34135) 2020-04-15 08:45:03 -07:00
Paulus Schoutsen
615a346a39 Merge pull request #34178 from home-assistant/rc
0.108.4
2020-04-13 18:17:03 -07:00
J. Nick Koston
d6e1bc3e75 Convert sense to use DataUpdateCoordinator for trends data (#34160)
* Convert sense to use DataUpdateCoordinator for trends

* remove unused

* request update right away

* clarify

* call async refresh later

* Update homeassistant/components/sense/__init__.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* Update homeassistant/components/sense/__init__.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-04-13 17:08:56 -07:00
Paulus Schoutsen
4e3414fc8a Bumped version to 0.108.4 2020-04-13 17:03:24 -07:00
J. Nick Koston
2553b0d1e0 Increase timeout and log the url of the elkm1 system that time… (#34172)
* Log the url of the elkm1 system that times out

* Bump timeout to 120s
2020-04-13 17:01:34 -07:00
J. Nick Koston
2d8bb8e6d2 Fix z-wave brightness off by one (#34170)
Z-wave would drop the floating point by calling
int() instead of round() which would result in
the brightness being off by one in many cases.
2020-04-13 17:01:33 -07:00
Jason Swails
e365f807c1 Improve rounding the light level conversion in Lutron Caseta (#34167) 2020-04-13 17:01:32 -07:00
Aaron Bach
87504806b1 Fix deprecated icon/username logic in Slack (#34156)
* Fix deprecated icon/username logic in Slack

* hassfest
2020-04-13 17:01:31 -07:00
James Nimmo
e742711a76 Bump pyIntesisHome to 1.7.3 (#34125) 2020-04-13 17:01:30 -07:00
Fredrik Erlandsson
667a87988d Fix Daikin sensor temperature_unit & cleanup (#34116) 2020-04-13 17:01:29 -07:00
Robert Svensson
4337dd6864 UniFi - Fix unit of measurement from B to MB (#34091) 2020-04-13 17:01:29 -07:00
J. Nick Koston
fc286900d3 Handle all zero serial numbers in NUT (#34045)
* Handle all zero serial numbers in NUT

* Add additional nut tests

* Update homeassistant/components/nut/__init__.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* remove re

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-04-13 17:00:58 -07:00
J. Nick Koston
908e044db1 Fix nexia fan and hold modes for XL824 thermostats (#34042)
* Fix nexia fan and hold modes for XL824 thermostats

* Update nexia to 0.9.0

* Update tests to reflect the modes that now come directly in
2020-04-13 16:54:42 -07:00
Kevin Eifinger
bb60286ed9 Fix #33995 Use "now" if departure is None (#34017) 2020-04-13 16:54:41 -07:00
Paulus Schoutsen
10799952af Merge pull request #33985 from home-assistant/rc
0.108.3
2020-04-10 15:24:17 -07:00
Chris Talkington
f00f3d6b0c Use zeroconf UUID if not available via IPP properties (#33991) 2020-04-10 15:23:59 -07:00
Paulus Schoutsen
d4dc7f806c Fix cherry pick ZHA 2020-04-10 14:39:37 -07:00
Paulus Schoutsen
c254b71559 Bumped version to 0.108.3 2020-04-10 14:12:12 -07:00
J. Nick Koston
d90a3b6c42 Exclude non thermostats from being detected by nexia (#33979)
* Fix detection of emergency heat

* Bump nexia to 0.8.2
2020-04-10 14:11:54 -07:00
David F. Mulcahey
da3ee0aa61 Cleanup ZHA group entity lifecycle (#33977)
* Clean up ZHA group entity lifecycle

* group entities don't use state restore

* add tests
2020-04-10 14:11:25 -07:00
Pascal Vizeli
eb17b68ad3 Fix shutdown timeout and make it upstream with Supervisor (#33973)
* Fix shutdown timeout and make it upstream with Supervisor

* Moved ENV command up

* Update finish

Co-authored-by: Franck Nijhof <git@frenck.dev>
2020-04-10 14:07:35 -07:00
J. Nick Koston
2243855209 Handle 304 http responses in nexia (#33972)
* Bump nexia to 0.8.1
2020-04-10 14:07:34 -07:00
Chris Talkington
82df4a3a4d Update pyipp to 0.9.2 (#33967)
* Update manifest.json

* Update requirements_test_all.txt

* Update requirements_all.txt
2020-04-10 14:07:33 -07:00
Knapoc
bc2ac65b1e Fix turning off/on light groups in homekit (#33965) 2020-04-10 14:07:33 -07:00
J. Nick Koston
9bc04d7b5c Fix powerwall units (kW) (#33954)
* Fix powerwall units (kW)

* Fix test

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-04-10 14:07:32 -07:00
J. Nick Koston
b620c53662 Make homekit aware of DEVICE_CLASS_GATE (#33936) 2020-04-10 14:07:31 -07:00
Minims
ab35ceab5a Fix Onvif Camera that does not have SnapshotUri such as Sricam (#33902) 2020-04-10 14:07:30 -07:00
58 changed files with 531 additions and 197 deletions

View File

@@ -77,7 +77,7 @@ homeassistant/components/counter/* @fabaff
homeassistant/components/cover/* @home-assistant/core
homeassistant/components/cpuspeed/* @fabaff
homeassistant/components/cups/* @fabaff
homeassistant/components/daikin/* @fredrike @rofrantz
homeassistant/components/daikin/* @fredrike
homeassistant/components/darksky/* @fabaff
homeassistant/components/deconz/* @kane610
homeassistant/components/delijn/* @bollewolle

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

@@ -347,12 +347,17 @@ class AmbientStation:
async def _attempt_connect(self):
"""Attempt to connect to the socket (retrying later on fail)."""
try:
async def connect(timestamp=None):
"""Connect."""
await self.client.websocket.connect()
try:
await connect()
except WebsocketError as err:
_LOGGER.error("Error with the websocket connection: %s", err)
self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480)
async_call_later(self._hass, self._ws_reconnect_delay, self.ws_connect)
async_call_later(self._hass, self._ws_reconnect_delay, connect)
async def ws_connect(self):
"""Register handlers and connect to the websocket."""

View File

@@ -482,7 +482,7 @@ async def google_assistant_list(hass, connection, msg):
{
"entity_id": entity.entity_id,
"traits": [trait.name for trait in entity.traits()],
"might_2fa": entity.might_2fa(),
"might_2fa": entity.might_2fa_traits(),
}
)

View File

@@ -17,6 +17,7 @@ from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import Throttle
from . import config_flow # noqa: F401
from .const import TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -91,7 +92,7 @@ async def daikin_api_setup(hass, host):
session = hass.helpers.aiohttp_client.async_get_clientsession()
try:
with timeout(10):
with timeout(TIMEOUT):
device = Appliance(host, session)
await device.init()
except asyncio.TimeoutError:

View File

@@ -84,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Daikin climate based on config_entry."""
daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id)
async_add_entities([DaikinClimate(daikin_api)])
async_add_entities([DaikinClimate(daikin_api)], update_before_add=True)
class DaikinClimate(ClimateDevice):

View File

@@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST
from .const import KEY_IP, KEY_MAC
from .const import KEY_IP, KEY_MAC, TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -38,7 +38,7 @@ class FlowHandler(config_entries.ConfigFlow):
device = Appliance(
host, self.hass.helpers.aiohttp_client.async_get_clientsession()
)
with timeout(10):
with timeout(TIMEOUT):
await device.init()
except asyncio.TimeoutError:
return self.async_abort(reason="device_timeout")

View File

@@ -25,3 +25,5 @@ SENSOR_TYPES = {
KEY_MAC = "mac"
KEY_IP = "ip"
TIMEOUT = 60

View File

@@ -3,8 +3,7 @@
"name": "Daikin AC",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/daikin",
"requirements": ["pydaikin==1.6.2"],
"dependencies": [],
"codeowners": ["@fredrike", "@rofrantz"],
"requirements": ["pydaikin==1.6.3"],
"codeowners": ["@fredrike"],
"quality_scale": "platinum"
}

View File

@@ -1,17 +1,11 @@
"""Support for Daikin AC sensors."""
import logging
from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE
from homeassistant.const import CONF_ICON, CONF_NAME, TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
from homeassistant.util.unit_system import UnitSystem
from . import DOMAIN as DAIKIN_DOMAIN
from .const import (
ATTR_INSIDE_TEMPERATURE,
ATTR_OUTSIDE_TEMPERATURE,
SENSOR_TYPE_TEMPERATURE,
SENSOR_TYPES,
)
from .const import ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, SENSOR_TYPES
_LOGGER = logging.getLogger(__name__)
@@ -31,30 +25,19 @@ async def async_setup_entry(hass, entry, async_add_entities):
sensors = [ATTR_INSIDE_TEMPERATURE]
if daikin_api.device.support_outside_temperature:
sensors.append(ATTR_OUTSIDE_TEMPERATURE)
async_add_entities(
[
DaikinClimateSensor(daikin_api, sensor, hass.config.units)
for sensor in sensors
]
)
async_add_entities([DaikinClimateSensor(daikin_api, sensor) for sensor in sensors])
class DaikinClimateSensor(Entity):
"""Representation of a Sensor."""
def __init__(self, api, monitored_state, units: UnitSystem, name=None) -> None:
def __init__(self, api, monitored_state) -> None:
"""Initialize the sensor."""
self._api = api
self._sensor = SENSOR_TYPES.get(monitored_state)
if name is None:
name = f"{self._sensor[CONF_NAME]} {api.name}"
self._name = f"{name} {monitored_state.replace('_', ' ')}"
self._sensor = SENSOR_TYPES[monitored_state]
self._name = f"{api.name} {self._sensor[CONF_NAME]}"
self._device_attribute = monitored_state
if self._sensor[CONF_TYPE] == SENSOR_TYPE_TEMPERATURE:
self._unit_of_measurement = units.temperature_unit
@property
def unique_id(self):
"""Return a unique ID."""
@@ -82,7 +65,7 @@ class DaikinClimateSensor(Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
return TEMP_CELSIUS
async def async_update(self):
"""Retrieve latest state."""

View File

@@ -39,7 +39,7 @@ from .const import (
ELK_ELEMENTS,
)
SYNC_TIMEOUT = 55
SYNC_TIMEOUT = 120
_LOGGER = logging.getLogger(__name__)
@@ -215,7 +215,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if not await async_wait_for_elk_to_sync(elk, SYNC_TIMEOUT):
_LOGGER.error(
"Timed out after %d seconds while trying to sync with ElkM1", SYNC_TIMEOUT,
"Timed out after %d seconds while trying to sync with ElkM1 at %s",
SYNC_TIMEOUT,
conf[CONF_HOST],
)
elk.disconnect()
raise ConfigEntryNotReady

View File

@@ -64,8 +64,9 @@ async def validate_input(data):
timed_out = False
if not await async_wait_for_elk_to_sync(elk, VALIDATE_TIMEOUT):
_LOGGER.error(
"Timed out after %d seconds while trying to sync with elkm1",
"Timed out after %d seconds while trying to sync with ElkM1 at %s",
VALIDATE_TIMEOUT,
url,
)
timed_out = True

View File

@@ -372,14 +372,19 @@ class GoogleEntity:
@callback
def might_2fa(self) -> bool:
"""Return if the entity might encounter 2FA."""
if not self.config.should_2fa(self.state):
return False
return self.might_2fa_traits()
@callback
def might_2fa_traits(self) -> bool:
"""Return if the entity might encounter 2FA based on just traits."""
state = self.state
domain = state.domain
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
if not self.config.should_2fa(state):
return False
return any(
trait.might_2fa(domain, features, device_class) for trait in self.traits()
)

View File

@@ -424,6 +424,9 @@ class HERETravelTimeData:
if departure is not None:
departure = convert_time_to_isodate(departure)
if departure is None and arrival is None:
departure = "now"
_LOGGER.debug(
"Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s, arrival: %s, departure: %s",
origin,

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

@@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/integrations/intesishome",
"dependencies": [],
"codeowners": ["@jnimmo"],
"requirements": ["pyintesishome==1.7.1"]
"requirements": ["pyintesishome==1.7.3"]
}

View File

@@ -6,8 +6,10 @@ from pyipp import (
IPP,
IPPConnectionError,
IPPConnectionUpgradeRequired,
IPPError,
IPPParseError,
IPPResponseError,
IPPVersionNotSupportedError,
)
import voluptuous as vol
@@ -70,10 +72,16 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
except IPPConnectionUpgradeRequired:
return self._show_setup_form({"base": "connection_upgrade"})
except (IPPConnectionError, IPPResponseError):
_LOGGER.debug("IPP Connection/Response Error", exc_info=True)
return self._show_setup_form({"base": "connection_error"})
except IPPParseError:
_LOGGER.exception("IPP Parse Error")
_LOGGER.debug("IPP Parse Error", exc_info=True)
return self.async_abort(reason="parse_error")
except IPPVersionNotSupportedError:
return self.async_abort(reason="ipp_version_error")
except IPPError:
_LOGGER.debug("IPP Error", exc_info=True)
return self.async_abort(reason="ipp_error")
user_input[CONF_UUID] = info[CONF_UUID]
@@ -111,12 +119,19 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
except IPPConnectionUpgradeRequired:
return self.async_abort(reason="connection_upgrade")
except (IPPConnectionError, IPPResponseError):
_LOGGER.debug("IPP Connection/Response Error", exc_info=True)
return self.async_abort(reason="connection_error")
except IPPParseError:
_LOGGER.exception("IPP Parse Error")
_LOGGER.debug("IPP Parse Error", exc_info=True)
return self.async_abort(reason="parse_error")
except IPPVersionNotSupportedError:
return self.async_abort(reason="ipp_version_error")
except IPPError:
_LOGGER.debug("IPP Error", exc_info=True)
return self.async_abort(reason="ipp_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.10.1"],
"codeowners": ["@ctalkington"],
"config_flow": true,
"quality_scale": "platinum",

View File

@@ -27,6 +27,8 @@
"already_configured": "This printer is already configured.",
"connection_error": "Failed to connect to printer.",
"connection_upgrade": "Failed to connect to printer due to connection upgrade being required.",
"ipp_error": "Encountered IPP error.",
"ipp_version_error": "IPP version not supported by printer.",
"parse_error": "Failed to parse response from printer."
}
}

View File

@@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
def to_lutron_level(level):
"""Convert the given Home Assistant light level (0-255) to Lutron (0-100)."""
return int((level * 100) // 255)
return int(round((level * 100) / 255))
def to_hass_level(level):

View File

@@ -2,7 +2,6 @@
import logging
from nexia.const import (
FAN_MODES,
OPERATION_MODE_AUTO,
OPERATION_MODE_COOL,
OPERATION_MODE_HEAT,
@@ -192,7 +191,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateDevice):
@property
def fan_modes(self):
"""Return the list of available fan modes."""
return FAN_MODES
return self._thermostat.get_fan_modes()
@property
def min_temp(self):

View File

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

View File

@@ -109,7 +109,7 @@ def _firmware_from_status(status):
def _serial_from_status(status):
"""Find the best serialvalue from the status."""
serial = status.get("device.serial") or status.get("ups.serial")
if serial and serial == "unknown":
if serial and (serial.lower() == "unknown" or serial.count("0") == len(serial)):
return None
return serial

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

@@ -17,6 +17,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
ACTIVE_UPDATE_RATE,
@@ -27,6 +28,7 @@ from .const import (
SENSE_DEVICES_DATA,
SENSE_DISCOVERED_DEVICES_DATA,
SENSE_TIMEOUT_EXCEPTIONS,
SENSE_TRENDS_COORDINATOR,
)
_LOGGER = logging.getLogger(__name__)
@@ -111,9 +113,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
except SENSE_TIMEOUT_EXCEPTIONS:
raise ConfigEntryNotReady
trends_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"Sense Trends {email}",
update_method=gateway.update_trend_data,
update_interval=timedelta(seconds=300),
)
# This can take longer than 60s and we already know
# sense is online since get_discovered_device_data was
# successful so we do it later.
hass.loop.create_task(trends_coordinator.async_request_refresh())
hass.data[DOMAIN][entry.entry_id] = {
SENSE_DATA: gateway,
SENSE_DEVICES_DATA: sense_devices_data,
SENSE_TRENDS_COORDINATOR: trends_coordinator,
SENSE_DISCOVERED_DEVICES_DATA: sense_discovered_devices,
}
@@ -122,7 +138,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.config_entries.async_forward_entry_setup(entry, component)
)
async def async_sense_update(now):
async def async_sense_update(_):
"""Retrieve latest state."""
try:
await gateway.update_realtime()

View File

@@ -71,7 +71,6 @@ class SenseDevice(BinarySensorDevice):
self._unique_id = f"{sense_monitor_id}-{self._id}"
self._icon = sense_to_mdi(device["icon"])
self._sense_devices_data = sense_devices_data
self._undo_dispatch_subscription = None
self._state = None
self._available = False
@@ -117,17 +116,14 @@ class SenseDevice(BinarySensorDevice):
async def async_added_to_hass(self):
"""Register callbacks."""
self._undo_dispatch_subscription = async_dispatcher_connect(
self.hass,
f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
self._async_update_from_data,
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
self._async_update_from_data,
)
)
async def async_will_remove_from_hass(self):
"""Undo subscription."""
if self._undo_dispatch_subscription:
self._undo_dispatch_subscription()
@callback
def _async_update_from_data(self):
"""Get the latest data, update state. Must not do I/O."""

View File

@@ -12,6 +12,7 @@ SENSE_DATA = "sense_data"
SENSE_DEVICE_UPDATE = "sense_devices_update"
SENSE_DEVICES_DATA = "sense_devices_data"
SENSE_DISCOVERED_DEVICES_DATA = "sense_discovered_devices"
SENSE_TRENDS_COORDINATOR = "sense_trends_coorindator"
ACTIVE_NAME = "Energy"
ACTIVE_TYPE = "active"

View File

@@ -1,12 +1,10 @@
"""Support for monitoring a Sense energy sensor."""
from datetime import timedelta
import logging
from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from .const import (
ACTIVE_NAME,
@@ -22,12 +20,9 @@ from .const import (
SENSE_DEVICE_UPDATE,
SENSE_DEVICES_DATA,
SENSE_DISCOVERED_DEVICES_DATA,
SENSE_TIMEOUT_EXCEPTIONS,
SENSE_TRENDS_COORDINATOR,
)
MIN_TIME_BETWEEN_DAILY_UPDATES = timedelta(seconds=300)
_LOGGER = logging.getLogger(__name__)
@@ -64,17 +59,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Sense sensor."""
data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DATA]
sense_devices_data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DEVICES_DATA]
trends_coordinator = hass.data[DOMAIN][config_entry.entry_id][
SENSE_TRENDS_COORDINATOR
]
@Throttle(MIN_TIME_BETWEEN_DAILY_UPDATES)
async def update_trends():
"""Update the daily power usage."""
await data.update_trend_data()
# Request only in case it takes longer
# than 60s
await trends_coordinator.async_request_refresh()
sense_monitor_id = data.sense_monitor_id
sense_devices = hass.data[DOMAIN][config_entry.entry_id][
SENSE_DISCOVERED_DEVICES_DATA
]
await data.update_trend_data()
devices = [
SenseEnergyDevice(sense_devices_data, device, sense_monitor_id)
@@ -108,8 +104,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
name,
sensor_type,
is_production,
update_trends,
var,
trends_coordinator,
unique_id,
)
)
@@ -140,7 +135,6 @@ class SenseActiveSensor(Entity):
self._sensor_type = sensor_type
self._is_production = is_production
self._state = None
self._undo_dispatch_subscription = None
@property
def name(self):
@@ -179,17 +173,14 @@ class SenseActiveSensor(Entity):
async def async_added_to_hass(self):
"""Register callbacks."""
self._undo_dispatch_subscription = async_dispatcher_connect(
self.hass,
f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
self._async_update_from_data,
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
self._async_update_from_data,
)
)
async def async_will_remove_from_hass(self):
"""Undo subscription."""
if self._undo_dispatch_subscription:
self._undo_dispatch_subscription()
@callback
def _async_update_from_data(self):
"""Update the sensor from the data. Must not do I/O."""
@@ -206,7 +197,7 @@ class SenseTrendsSensor(Entity):
"""Implementation of a Sense energy sensor."""
def __init__(
self, data, name, sensor_type, is_production, update_call, sensor_id, unique_id
self, data, name, sensor_type, is_production, trends_coordinator, unique_id,
):
"""Initialize the Sense sensor."""
name_type = PRODUCTION_NAME if is_production else CONSUMPTION_NAME
@@ -215,10 +206,11 @@ class SenseTrendsSensor(Entity):
self._available = False
self._data = data
self._sensor_type = sensor_type
self.update_sensor = update_call
self._coordinator = trends_coordinator
self._is_production = is_production
self._state = None
self._unit_of_measurement = ENERGY_KILO_WATT_HOUR
self._had_any_update = False
@property
def name(self):
@@ -228,12 +220,12 @@ class SenseTrendsSensor(Entity):
@property
def state(self):
"""Return the state of the sensor."""
return self._state
return round(self._data.get_trend(self._sensor_type, self._is_production), 1)
@property
def available(self):
"""Return the availability of the sensor."""
return self._available
"""Return if entity is available."""
return self._had_any_update and self._coordinator.last_update_success
@property
def unit_of_measurement(self):
@@ -250,18 +242,27 @@ class SenseTrendsSensor(Entity):
"""Return the unique id."""
return self._unique_id
@property
def should_poll(self):
"""No need to poll. Coordinator notifies entity of updates."""
return False
@callback
def _async_update(self):
"""Track if we had an update so we do not report zero data."""
self._had_any_update = True
self.async_write_ha_state()
async def async_update(self):
"""Get the latest data, update state."""
"""Update the entity.
try:
await self.update_sensor()
except SENSE_TIMEOUT_EXCEPTIONS:
_LOGGER.error("Timeout retrieving data")
return
Only used by the generic entity update service.
"""
await self._coordinator.async_request_refresh()
state = self._data.get_trend(self._sensor_type, self._is_production)
self._state = round(state, 1)
self._available = True
async def async_added_to_hass(self):
"""When entity is added to hass."""
self.async_on_remove(self._coordinator.async_add_listener(self._async_update))
class SenseEnergyDevice(Entity):
@@ -276,7 +277,6 @@ class SenseEnergyDevice(Entity):
self._unique_id = f"{sense_monitor_id}-{self._id}-{CONSUMPTION_ID}"
self._icon = sense_to_mdi(device["icon"])
self._sense_devices_data = sense_devices_data
self._undo_dispatch_subscription = None
self._state = None
@property
@@ -321,17 +321,14 @@ class SenseEnergyDevice(Entity):
async def async_added_to_hass(self):
"""Register callbacks."""
self._undo_dispatch_subscription = async_dispatcher_connect(
self.hass,
f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
self._async_update_from_data,
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
self._async_update_from_data,
)
)
async def async_will_remove_from_hass(self):
"""Undo subscription."""
if self._undo_dispatch_subscription:
self._undo_dispatch_subscription()
@callback
def _async_update_from_data(self):
"""Get the latest data, update state. Must not do I/O."""

View File

@@ -74,11 +74,7 @@ class SlackNotificationService(BaseNotificationService):
self._default_channel = default_channel
self._hass = hass
self._icon = icon
if username or self._icon:
self._as_user = False
else:
self._as_user = True
self._username = username
async def _async_send_local_file_message(self, path, targets, message, title):
"""Upload a local file (with message) to Slack."""
@@ -108,11 +104,11 @@ class SlackNotificationService(BaseNotificationService):
target: self._client.chat_postMessage(
channel=target,
text=message,
as_user=self._as_user,
attachments=attachments,
blocks=blocks,
icon_emoji=self._icon,
link_names=True,
username=self._username,
)
for target in targets
}

View File

@@ -2,7 +2,7 @@
import logging
from homeassistant.components.unifi.config_flow import get_controller_from_config_entry
from homeassistant.const import DATA_BYTES
from homeassistant.const import DATA_MEGABYTES
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -116,7 +116,7 @@ class UniFiRxBandwidthSensor(UniFiClient):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return DATA_BYTES
return DATA_MEGABYTES
class UniFiTxBandwidthSensor(UniFiRxBandwidthSensor):

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

@@ -104,7 +104,7 @@ def byte_to_zwave_brightness(value):
`value` -- (int) Brightness byte value from 0-255.
"""
if value > 0:
return max(1, int((value / 255) * 99))
return max(1, round((value / 255) * 99))
return 0

View File

@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 108
PATCH_VERSION = "2"
PATCH_VERSION = "5"
__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.9.1
# homeassistant.components.nextcloud
nextcloudmonitor==1.1.0
@@ -1218,7 +1218,7 @@ pycsspeechtts==1.0.3
# pycups==1.9.73
# homeassistant.components.daikin
pydaikin==1.6.2
pydaikin==1.6.3
# homeassistant.components.danfoss_air
pydanfossair==0.1.0
@@ -1330,13 +1330,13 @@ pyialarm==0.3
pyicloud==0.9.6.1
# homeassistant.components.intesishome
pyintesishome==1.7.1
pyintesishome==1.7.3
# homeassistant.components.ipma
pyipma==2.0.5
# homeassistant.components.ipp
pyipp==0.9.1
pyipp==0.10.1
# 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.9.1
# homeassistant.components.nsw_fuel_station
nsw-fuel-api-client==1.0.10
@@ -479,7 +479,7 @@ pychromecast==4.2.0
pycoolmasternet==0.0.4
# homeassistant.components.daikin
pydaikin==1.6.2
pydaikin==1.6.3
# homeassistant.components.deconz
pydeconz==70
@@ -519,7 +519,7 @@ pyicloud==0.9.6.1
pyipma==2.0.5
# homeassistant.components.ipp
pyipp==0.9.1
pyipp==0.10.1
# 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

23
rootfs/init Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/execlineb -S0
##
## load default PATH (the same that Docker includes if not provided) if it doesn't exist,
## then go ahead with stage1.
## this was motivated due to this issue:
## - https://github.com/just-containers/s6-overlay/issues/108
##
/bin/importas -D /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PATH PATH
export PATH ${PATH}
##
## Skip further init if the user has a given CMD.
## This is to prevent Home Assistant from starting twice if the user
## decided to override/start via the CMD.
##
ifelse { s6-test $# -ne 0 }
{
$@
}
/etc/s6/init/init-stage1 $@

View File

@@ -687,20 +687,30 @@ async def test_list_google_entities(hass, hass_ws_client, setup_api, mock_cloud_
entity = GoogleEntity(
hass, MockConfig(should_expose=lambda *_: False), State("light.kitchen", "on")
)
entity2 = GoogleEntity(
hass,
MockConfig(should_expose=lambda *_: True, should_2fa=lambda *_: False),
State("cover.garage", "open", {"device_class": "garage"}),
)
with patch(
"homeassistant.components.google_assistant.helpers.async_get_entities",
return_value=[entity],
return_value=[entity, entity2],
):
await client.send_json({"id": 5, "type": "cloud/google_assistant/entities"})
response = await client.receive_json()
assert response["success"]
assert len(response["result"]) == 1
assert len(response["result"]) == 2
assert response["result"][0] == {
"entity_id": "light.kitchen",
"might_2fa": False,
"traits": ["action.devices.traits.OnOff"],
}
assert response["result"][1] == {
"entity_id": "cover.garage",
"might_2fa": True,
"traits": ["action.devices.traits.OpenClose"],
}
async def test_update_google_entity(hass, hass_ws_client, setup_api, mock_cloud_login):

View File

@@ -33,6 +33,7 @@ class MockConfig(helpers.AbstractConfig):
"""Initialize config."""
super().__init__(hass)
self._should_expose = should_expose
self._should_2fa = should_2fa
self._secure_devices_pin = secure_devices_pin
self._entity_config = entity_config or {}
self._local_sdk_webhook_id = local_sdk_webhook_id
@@ -73,6 +74,10 @@ class MockConfig(helpers.AbstractConfig):
"""Expose it all."""
return self._should_expose is None or self._should_expose(state)
def should_2fa(self, state):
"""Expose it all."""
return self._should_2fa is None or self._should_2fa(state)
BASIC_CONFIG = MockConfig()

View File

@@ -845,10 +845,8 @@ async def test_lock_unlock_unlock(hass):
assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP
# Test with 2FA override
with patch(
"homeassistant.components.google_assistant.helpers"
".AbstractConfig.should_2fa",
return_value=False,
with patch.object(
BASIC_CONFIG, "should_2fa", return_value=False,
):
await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {"lock": False}, {})
assert len(calls) == 2

View File

@@ -80,6 +80,8 @@ def _build_mock_url(origin, destination, modes, api_key, departure=None, arrival
parameters["arrival"] = arrival
if departure is not None:
parameters["departure"] = departure
if departure is None and arrival is None:
parameters["departure"] = "now"
url = base_url + urllib.parse.urlencode(parameters)
print(url)
return url

View File

@@ -1,6 +1,6 @@
"""Tests for the IPP config flow."""
import aiohttp
from pyipp import IPPConnectionUpgradeRequired
from pyipp import IPPConnectionUpgradeRequired, IPPError
from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
@@ -172,6 +172,74 @@ async def test_zeroconf_parse_error(
assert result["reason"] == "parse_error"
async def test_user_ipp_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort the user flow on IPP error."""
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError)
user_input = MOCK_USER_INPUT.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=user_input,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "ipp_error"
async def test_zeroconf_ipp_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on IPP error."""
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "ipp_error"
async def test_user_ipp_version_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort user flow on IPP version not supported error."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"),
headers={"Content-Type": "application/ipp"},
)
user_input = {**MOCK_USER_INPUT}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=user_input,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "ipp_version_error"
async def test_zeroconf_ipp_version_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on IPP version not supported error."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"),
headers={"Content-Type": "application/ipp"},
)
discovery_info = {**MOCK_ZEROCONF_IPP_SERVICE_INFO}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "ipp_version_error"
async def test_user_device_exists_abort(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:

View File

@@ -18,8 +18,8 @@ async def test_climate_zones(hass):
"current_temperature": 22.8,
"dehumidify_setpoint": 45.0,
"dehumidify_supported": True,
"fan_mode": "auto",
"fan_modes": ["auto", "on", "circulate"],
"fan_mode": "Auto",
"fan_modes": ["Auto", "On", "Circulate"],
"friendly_name": "Nick Office",
"humidify_supported": False,
"humidity": 45.0,
@@ -53,8 +53,8 @@ async def test_climate_zones(hass):
"current_temperature": 25.0,
"dehumidify_setpoint": 50.0,
"dehumidify_supported": True,
"fan_mode": "auto",
"fan_modes": ["auto", "on", "circulate"],
"fan_mode": "Auto",
"fan_modes": ["Auto", "On", "Circulate"],
"friendly_name": "Kitchen",
"humidify_supported": False,
"humidity": 50.0,

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

View File

@@ -100,13 +100,23 @@ def test_dimmer_turn_on(mock_openzwave):
node.reset_mock()
device.turn_on(**{ATTR_BRIGHTNESS: 224})
assert node.set_dimmer.called
value_id, brightness = node.set_dimmer.mock_calls[0][1]
assert value_id == value.value_id
assert brightness == 87 # round(224 / 255 * 99)
node.reset_mock()
device.turn_on(**{ATTR_BRIGHTNESS: 120})
assert node.set_dimmer.called
value_id, brightness = node.set_dimmer.mock_calls[0][1]
assert value_id == value.value_id
assert brightness == 46 # int(120 / 255 * 99)
assert brightness == 47 # round(120 / 255 * 99)
with patch.object(light, "_LOGGER", MagicMock()) as mock_logger:
device.turn_on(**{ATTR_TRANSITION: 35})

Binary file not shown.

47
tests/fixtures/nut/BACKUPSES600M1.json vendored Normal file
View File

@@ -0,0 +1,47 @@
{
"ups.realpower.nominal" : "330",
"input.voltage" : "123.0",
"ups.mfr" : "American Power Conversion",
"driver.version" : "2.7.4",
"ups.test.result" : "No test initiated",
"input.voltage.nominal" : "120",
"input.transfer.low" : "92",
"driver.parameter.pollinterval" : "15",
"driver.version.data" : "APC HID 0.96",
"driver.parameter.pollfreq" : "30",
"battery.mfr.date" : "2017/04/01",
"ups.beeper.status" : "enabled",
"battery.date" : "2001/09/25",
"driver.name" : "usbhid-ups",
"battery.charge" : "100",
"ups.status" : "OL",
"ups.model" : "Back-UPS ES 600M1",
"battery.runtime.low" : "120",
"ups.firmware" : "928.a5 .D",
"ups.delay.shutdown" : "20",
"device.model" : "Back-UPS ES 600M1",
"device.serial" : "4B1713P32195 ",
"input.sensitivity" : "medium",
"ups.firmware.aux" : "a5 ",
"input.transfer.reason" : "input voltage out of range",
"ups.timer.reboot" : "0",
"battery.voltage.nominal" : "12.0",
"ups.vendorid" : "051d",
"input.transfer.high" : "139",
"battery.voltage" : "13.7",
"battery.charge.low" : "10",
"battery.type" : "PbAc",
"ups.mfr.date" : "2017/04/01",
"ups.timer.shutdown" : "-1",
"device.mfr" : "American Power Conversion",
"driver.parameter.port" : "auto",
"battery.charge.warning" : "50",
"device.type" : "ups",
"driver.parameter.vendorid" : "051d",
"ups.serial" : "4B1713P32195 ",
"ups.load" : "22",
"driver.version.internal" : "0.41",
"battery.runtime" : "1968",
"driver.parameter.synchronous" : "no",
"ups.productid" : "0002"
}

43
tests/fixtures/nut/CP1500PFCLCD.json vendored Normal file
View File

@@ -0,0 +1,43 @@
{
"battery.runtime.low" : "300",
"driver.parameter.port" : "auto",
"ups.delay.shutdown" : "20",
"driver.parameter.pollfreq" : "30",
"ups.beeper.status" : "disabled",
"input.voltage.nominal" : "120",
"device.serial" : "000000000000",
"ups.timer.shutdown" : "-60",
"input.voltage" : "122.0",
"ups.status" : "OL",
"ups.model" : "CP1500PFCLCD",
"device.mfr" : "CPS",
"device.model" : "CP1500PFCLCD",
"input.transfer.low" : "88",
"battery.mfr.date" : "CPS",
"driver.version" : "2.7.4",
"driver.version.data" : "CyberPower HID 0.4",
"driver.parameter.synchronous" : "no",
"ups.realpower.nominal" : "900",
"ups.productid" : "0501",
"ups.mfr" : "CPS",
"ups.vendorid" : "0764",
"driver.version.internal" : "0.41",
"output.voltage" : "138.0",
"battery.runtime" : "10530",
"device.type" : "ups",
"battery.charge.low" : "10",
"ups.timer.start" : "-60",
"driver.parameter.pollinterval" : "15",
"ups.load" : "0",
"ups.serial" : "000000000000",
"input.transfer.high" : "139",
"battery.charge.warning" : "20",
"battery.voltage.nominal" : "24",
"driver.parameter.vendorid" : "0764",
"driver.name" : "usbhid-ups",
"battery.type" : "PbAcid",
"ups.delay.start" : "30",
"battery.voltage" : "24.0",
"battery.charge" : "100",
"ups.test.result" : "No test initiated"
}