Compare commits

...

37 Commits

Author SHA1 Message Date
Paulus Schoutsen
d0ada6c6e2 Bumped version to 2021.9.0b2 2021-08-27 10:00:20 -07:00
Anders Melchiorsen
76bb036968 Upgrade aiolifx to 0.6.10 (#55344) 2021-08-27 10:00:00 -07:00
J. Nick Koston
d8b64be41c Retrigger config flow when the ssdp location changes for a UDN (#55343)
Fixes #55229
2021-08-27 09:59:59 -07:00
jan iversen
b3e0b7b86e Add modbus name to log_error (#55336) 2021-08-27 09:59:59 -07:00
Chris Talkington
e097e4c1c2 Fix reauth for sonarr (#55329)
* fix reauth for sonarr

* Update config_flow.py

* Update config_flow.py

* Update config_flow.py

* Update test_config_flow.py

* Update config_flow.py

* Update test_config_flow.py

* Update config_flow.py
2021-08-27 09:59:58 -07:00
Robert Hillis
34f0fecef8 Fix sonos alarm schema (#55318) 2021-08-27 09:59:57 -07:00
Erik Montnemery
f53a10d39a Handle statistics for sensor with changing state class (#55316) 2021-08-27 09:59:56 -07:00
J. Nick Koston
5b993129d6 Fix lifx model to be a string (#55309)
Fixes #55307
2021-08-27 09:59:56 -07:00
J. Nick Koston
865656d436 Always send powerview move command in case shade is out of sync (#55308) 2021-08-27 09:59:55 -07:00
Aaron Bach
fb25c6c115 Bump simplisafe-python to 11.0.5 (#55306) 2021-08-27 09:59:54 -07:00
Aaron Bach
c963cf8743 Bump aiorecollect to 1.0.8 (#55300) 2021-08-27 09:59:54 -07:00
rikroe
ddb28db21a Bump bimmer_connected to 0.7.20 (#55299) 2021-08-27 09:59:53 -07:00
J. Nick Koston
bfc98b444f Fix creation of new nmap tracker entities (#55297) 2021-08-27 09:59:52 -07:00
realPy
f9a0f44137 Correct flash light livarno when use hue (#55294)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2021-08-27 09:59:51 -07:00
J. Nick Koston
93750d71ce Gracefully handle pyudev failing to filter on WSL (#55286)
* Gracefully handle pyudev failing to filter on WSL

* add debug message

* add mocks so we reach the new check
2021-08-27 09:59:51 -07:00
Paulus Schoutsen
06e4003640 Bump ring to 0.7.1 (#55282) 2021-08-27 09:59:50 -07:00
J. Nick Koston
97ff5e2085 Set yeelight capabilities from external discovery (#55280) 2021-08-27 09:59:49 -07:00
J. Nick Koston
8a2c07ce19 Ensure yeelight model is set in the config entry (#55281)
* Ensure yeelight model is set in the config entry

- If the model was not set in the config entry the light could
  be sent commands it could not handle

* update tests

* fix test
2021-08-27 09:59:21 -07:00
J. Nick Koston
9f7398e0df Fix yeelight brightness when nightlight switch is disabled (#55278) 2021-08-27 09:57:07 -07:00
J. Nick Koston
7df84dadad Fix some yeelights showing wrong state after on/off (#55279) 2021-08-27 09:56:22 -07:00
Chris
2a1e943b18 Fix unique_id conflict in smarttthings (#55235) 2021-08-27 09:54:26 -07:00
prwood80
e6e72bfa82 Improve performance of ring camera still images (#53803)
Co-authored-by: Pat Wood <prwood80@users.noreply.github.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2021-08-27 09:54:25 -07:00
Paulus Schoutsen
219868b308 Bumped version to 2021.9.0b1 2021-08-26 09:37:25 -07:00
Maciej Bieniek
67dd861d8c Fix AttributeError for non-MIOT Xiaomi Miio purifiers (#55271) 2021-08-26 09:37:20 -07:00
Florian Gareis
f2765ba320 Don't create DSL sensor for devices that don't support DSL (#55269) 2021-08-26 09:37:19 -07:00
Erik Montnemery
aefd3df914 Warn if a sensor with state_class_total has a decreasing value twice (#55251) 2021-08-26 09:37:18 -07:00
Franck Nijhof
3658eeb8d1 Fix MQTT add-on discovery to be ignorable (#55250) 2021-08-26 09:37:07 -07:00
Erik Montnemery
080cb6b6e9 Fix double precision float for postgresql (#55249) 2021-08-26 09:37:06 -07:00
Joakim Sørensen
20796303da Only postfix image name for container (#55248) 2021-08-26 09:37:06 -07:00
J. Nick Koston
dff6151ff4 Abort zha usb discovery if deconz is setup (#55245)
* Abort zha usb discovery if deconz is setup

* Update tests/components/zha/test_config_flow.py

* add deconz domain const

* Update homeassistant/components/zha/config_flow.py

Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>

Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
2021-08-26 09:37:05 -07:00
Alexei Chetroi
6f24f4e302 Bump up ZHA dependencies (#55242)
* Bump up ZHA dependencies

* Bump up zha-device-handlers
2021-08-26 09:37:04 -07:00
J. Nick Koston
175febe635 Defer zha auto configure probe until after clicking configure (#55239) 2021-08-26 09:37:03 -07:00
J. Nick Koston
aa907f4d10 Only warn once per entity when the async_camera_image signature needs to be updated (#55238) 2021-08-26 09:37:02 -07:00
J. Nick Koston
3d09478aea Limit USB discovery to specific manufacturer/description/serial_number matches (#55236)
* Limit USB discovery to specific manufacturer/description/serial_number matches

* test for None case
2021-08-26 09:37:01 -07:00
Marc Mueller
05df9b4b8b Remove temperature conversion - tado (#55231) 2021-08-26 09:37:01 -07:00
jjlawren
1865a28083 Set up polling task with subscriptions in Sonos (#54355) 2021-08-26 09:37:00 -07:00
Franck Nijhof
f78d57515a Bumped version to 2021.9.0b0 2021-08-25 22:11:21 +02:00
54 changed files with 1159 additions and 234 deletions

View File

@@ -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.19"],
"requirements": ["bimmer_connected==0.7.20"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true,
"iot_class": "cloud_polling"

View File

@@ -165,10 +165,7 @@ async def _async_get_image(
width=width, height=height
)
else:
_LOGGER.warning(
"The camera entity %s does not support requesting width and height, please open an issue with the integration author",
camera.entity_id,
)
camera.async_warn_old_async_camera_image_signature()
image_bytes = await camera.async_camera_image()
if image_bytes:
@@ -381,6 +378,7 @@ class Camera(Entity):
self.stream_options: dict[str, str] = {}
self.content_type: str = DEFAULT_CONTENT_TYPE
self.access_tokens: collections.deque = collections.deque([], 2)
self._warned_old_signature = False
self.async_update_token()
@property
@@ -455,11 +453,20 @@ class Camera(Entity):
return await self.hass.async_add_executor_job(
partial(self.camera_image, width=width, height=height)
)
self.async_warn_old_async_camera_image_signature()
return await self.hass.async_add_executor_job(self.camera_image)
# Remove in 2022.1 after all custom components have had a chance to change their signature
@callback
def async_warn_old_async_camera_image_signature(self) -> None:
"""Warn once when calling async_camera_image with the function old signature."""
if self._warned_old_signature:
return
_LOGGER.warning(
"The camera entity %s does not support requesting width and height, please open an issue with the integration author",
self.entity_id,
)
return await self.hass.async_add_executor_job(self.camera_image)
self._warned_old_signature = True
async def handle_async_still_stream(
self, request: web.Request, interval: float

View File

@@ -5,7 +5,12 @@ import datetime
import logging
from typing import Callable, TypedDict
from fritzconnection.core.exceptions import FritzConnectionException
from fritzconnection.core.exceptions import (
FritzActionError,
FritzActionFailedError,
FritzConnectionException,
FritzServiceError,
)
from fritzconnection.lib.fritzstatus import FritzStatus
from homeassistant.components.sensor import (
@@ -260,12 +265,16 @@ async def async_setup_entry(
return
entities = []
dslinterface = await hass.async_add_executor_job(
fritzbox_tools.connection.call_action,
"WANDSLInterfaceConfig:1",
"GetInfo",
)
dsl: bool = dslinterface["NewEnable"]
dsl: bool = False
try:
dslinterface = await hass.async_add_executor_job(
fritzbox_tools.connection.call_action,
"WANDSLInterfaceConfig:1",
"GetInfo",
)
dsl = dslinterface["NewEnable"]
except (FritzActionError, FritzActionFailedError, FritzServiceError):
pass
for sensor_type, sensor_data in SENSOR_DATA.items():
if not dsl and sensor_data.get("connection_type") == DSL_CONNECTION:

View File

@@ -282,12 +282,14 @@ class HueLight(CoordinatorEntity, LightEntity):
self.is_osram = False
self.is_philips = False
self.is_innr = False
self.is_livarno = False
self.gamut_typ = GAMUT_TYPE_UNAVAILABLE
self.gamut = None
else:
self.is_osram = light.manufacturername == "OSRAM"
self.is_philips = light.manufacturername == "Philips"
self.is_innr = light.manufacturername == "innr"
self.is_livarno = light.manufacturername.startswith("_TZ3000_")
self.gamut_typ = self.light.colorgamuttype
self.gamut = self.light.colorgamut
_LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut))
@@ -383,6 +385,8 @@ class HueLight(CoordinatorEntity, LightEntity):
"""Return the warmest color_temp that this light supports."""
if self.is_group:
return super().max_mireds
if self.is_livarno:
return 500
max_mireds = self.light.controlcapabilities.get("ct", {}).get("max")
@@ -493,7 +497,7 @@ class HueLight(CoordinatorEntity, LightEntity):
elif flash == FLASH_SHORT:
command["alert"] = "select"
del command["on"]
elif not self.is_innr:
elif not self.is_innr and not self.is_livarno:
command["alert"] = "none"
if ATTR_EFFECT in kwargs:
@@ -532,7 +536,7 @@ class HueLight(CoordinatorEntity, LightEntity):
elif flash == FLASH_SHORT:
command["alert"] = "select"
del command["on"]
elif not self.is_innr:
elif not self.is_innr and not self.is_livarno:
command["alert"] = "none"
if self.is_group:

View File

@@ -177,8 +177,6 @@ class PowerViewShade(ShadeEntity, CoverEntity):
"""Move the shade to a position."""
current_hass_position = hd_position_to_hass(self._current_cover_position)
steps_to_move = abs(current_hass_position - target_hass_position)
if not steps_to_move:
return
self._async_schedule_update_for_transition(steps_to_move)
self._async_update_from_command(
await self._shade.move(

View File

@@ -470,7 +470,7 @@ class LIFXLight(LightEntity):
model = product_map.get(self.bulb.product) or self.bulb.product
if model is not None:
info["model"] = model
info["model"] = str(model)
return info

View File

@@ -3,7 +3,7 @@
"name": "LIFX",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lifx",
"requirements": ["aiolifx==0.6.9", "aiolifx_effects==0.2.2"],
"requirements": ["aiolifx==0.6.10", "aiolifx_effects==0.2.2"],
"homekit": {
"models": ["LIFX"]
},

View File

@@ -243,7 +243,7 @@ class ModbusHub:
self._msg_wait = 0
def _log_error(self, text: str, error_state=True):
log_text = f"Pymodbus: {text}"
log_text = f"Pymodbus: {self.name}: {text}"
if self._in_error:
_LOGGER.debug(log_text)
else:

View File

@@ -95,8 +95,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_hassio(self, discovery_info):
"""Receive a Hass.io discovery."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
await self._async_handle_discovery_without_unique_id()
self._hassio_discovery = discovery_info

View File

@@ -20,6 +20,7 @@
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
},
"error": {

View File

@@ -1,6 +1,7 @@
{
"config": {
"abort": {
"already_configured": "Service is already configured",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {

View File

@@ -361,13 +361,6 @@ class NmapDeviceScanner:
continue
formatted_mac = format_mac(mac)
new = formatted_mac not in devices.tracked
if (
new
and formatted_mac not in devices.tracked
and formatted_mac not in self._known_mac_addresses
):
continue
if (
devices.config_entry_owner.setdefault(formatted_mac, entry_id)
@@ -382,6 +375,7 @@ class NmapDeviceScanner:
formatted_mac, hostname, name, ipv4, vendor, reason, now, 0
)
new = formatted_mac not in devices.tracked
devices.tracked[formatted_mac] = device
devices.ipv4_last_mac[ipv4] = formatted_mac
self._last_results.append(device)

View File

@@ -3,7 +3,7 @@
"name": "ReCollect Waste",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/recollect_waste",
"requirements": ["aiorecollect==1.0.7"],
"requirements": ["aiorecollect==1.0.8"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling"
}

View File

@@ -20,7 +20,6 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.util.dt import as_utc
from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER
@@ -124,7 +123,7 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity):
ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names(
self._entry, next_pickup_event.pickup_types
),
ATTR_NEXT_PICKUP_DATE: as_utc(next_pickup_event.date).isoformat(),
ATTR_NEXT_PICKUP_DATE: next_pickup_event.date.isoformat(),
}
)
self._attr_native_value = as_utc(pickup_event.date).isoformat()
self._attr_native_value = pickup_event.date.isoformat()

View File

@@ -70,7 +70,7 @@ DOUBLE_TYPE = (
Float()
.with_variant(mysql.DOUBLE(asdecimal=False), "mysql")
.with_variant(oracle.DOUBLE_PRECISION(), "oracle")
.with_variant(postgresql.DOUBLE_PRECISION, "postgresql")
.with_variant(postgresql.DOUBLE_PRECISION(), "postgresql")
)
@@ -267,6 +267,7 @@ class Statistics(Base): # type: ignore
class StatisticMetaData(TypedDict, total=False):
"""Statistic meta data class."""
statistic_id: str
unit_of_measurement: str | None
has_mean: bool
has_sum: bool

View File

@@ -53,6 +53,13 @@ QUERY_STATISTIC_META = [
StatisticsMeta.id,
StatisticsMeta.statistic_id,
StatisticsMeta.unit_of_measurement,
StatisticsMeta.has_mean,
StatisticsMeta.has_sum,
]
QUERY_STATISTIC_META_ID = [
StatisticsMeta.id,
StatisticsMeta.statistic_id,
]
STATISTICS_BAKERY = "recorder_statistics_bakery"
@@ -124,33 +131,61 @@ def _get_metadata_ids(
) -> list[str]:
"""Resolve metadata_id for a list of statistic_ids."""
baked_query = hass.data[STATISTICS_META_BAKERY](
lambda session: session.query(*QUERY_STATISTIC_META)
lambda session: session.query(*QUERY_STATISTIC_META_ID)
)
baked_query += lambda q: q.filter(
StatisticsMeta.statistic_id.in_(bindparam("statistic_ids"))
)
result = execute(baked_query(session).params(statistic_ids=statistic_ids))
return [id for id, _, _ in result] if result else []
return [id for id, _ in result] if result else []
def _get_or_add_metadata_id(
def _update_or_add_metadata(
hass: HomeAssistant,
session: scoped_session,
statistic_id: str,
metadata: StatisticMetaData,
new_metadata: StatisticMetaData,
) -> str:
"""Get metadata_id for a statistic_id, add if it doesn't exist."""
metadata_id = _get_metadata_ids(hass, session, [statistic_id])
if not metadata_id:
unit = metadata["unit_of_measurement"]
has_mean = metadata["has_mean"]
has_sum = metadata["has_sum"]
old_metadata_dict = _get_metadata(hass, session, [statistic_id], None)
if not old_metadata_dict:
unit = new_metadata["unit_of_measurement"]
has_mean = new_metadata["has_mean"]
has_sum = new_metadata["has_sum"]
session.add(
StatisticsMeta.from_meta(DOMAIN, statistic_id, unit, has_mean, has_sum)
)
metadata_id = _get_metadata_ids(hass, session, [statistic_id])
return metadata_id[0]
metadata_ids = _get_metadata_ids(hass, session, [statistic_id])
_LOGGER.debug(
"Added new statistics metadata for %s, new_metadata: %s",
statistic_id,
new_metadata,
)
return metadata_ids[0]
metadata_id, old_metadata = next(iter(old_metadata_dict.items()))
if (
old_metadata["has_mean"] != new_metadata["has_mean"]
or old_metadata["has_sum"] != new_metadata["has_sum"]
or old_metadata["unit_of_measurement"] != new_metadata["unit_of_measurement"]
):
session.query(StatisticsMeta).filter_by(statistic_id=statistic_id).update(
{
StatisticsMeta.has_mean: new_metadata["has_mean"],
StatisticsMeta.has_sum: new_metadata["has_sum"],
StatisticsMeta.unit_of_measurement: new_metadata["unit_of_measurement"],
},
synchronize_session=False,
)
_LOGGER.debug(
"Updated statistics metadata for %s, old_metadata: %s, new_metadata: %s",
statistic_id,
old_metadata,
new_metadata,
)
return metadata_id
@retryable_database_job("statistics")
@@ -177,7 +212,7 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool:
with session_scope(session=instance.get_session()) as session: # type: ignore
for stats in platform_stats:
for entity_id, stat in stats.items():
metadata_id = _get_or_add_metadata_id(
metadata_id = _update_or_add_metadata(
instance.hass, session, entity_id, stat["meta"]
)
session.add(Statistics.from_stats(metadata_id, start, stat["stat"]))
@@ -191,14 +226,19 @@ def _get_metadata(
session: scoped_session,
statistic_ids: list[str] | None,
statistic_type: str | None,
) -> dict[str, dict[str, str]]:
) -> dict[str, StatisticMetaData]:
"""Fetch meta data."""
def _meta(metas: list, wanted_metadata_id: str) -> dict[str, str] | None:
meta = None
for metadata_id, statistic_id, unit in metas:
def _meta(metas: list, wanted_metadata_id: str) -> StatisticMetaData | None:
meta: StatisticMetaData | None = None
for metadata_id, statistic_id, unit, has_mean, has_sum in metas:
if metadata_id == wanted_metadata_id:
meta = {"unit_of_measurement": unit, "statistic_id": statistic_id}
meta = {
"statistic_id": statistic_id,
"unit_of_measurement": unit,
"has_mean": has_mean,
"has_sum": has_sum,
}
return meta
baked_query = hass.data[STATISTICS_META_BAKERY](
@@ -219,7 +259,7 @@ def _get_metadata(
return {}
metadata_ids = [metadata[0] for metadata in result]
metadata = {}
metadata: dict[str, StatisticMetaData] = {}
for _id in metadata_ids:
meta = _meta(result, _id)
if meta:
@@ -230,7 +270,7 @@ def _get_metadata(
def get_metadata(
hass: HomeAssistant,
statistic_id: str,
) -> dict[str, str] | None:
) -> StatisticMetaData | None:
"""Return metadata for a statistic_id."""
statistic_ids = [statistic_id]
with session_scope(hass=hass) as session:
@@ -255,7 +295,7 @@ def _configured_unit(unit: str, units: UnitSystem) -> str:
def list_statistic_ids(
hass: HomeAssistant, statistic_type: str | None = None
) -> list[dict[str, str] | None]:
) -> list[StatisticMetaData | None]:
"""Return statistic_ids and meta data."""
units = hass.config.units
statistic_ids = {}
@@ -263,7 +303,9 @@ def list_statistic_ids(
metadata = _get_metadata(hass, session, None, statistic_type)
for meta in metadata.values():
unit = _configured_unit(meta["unit_of_measurement"], units)
unit = meta["unit_of_measurement"]
if unit is not None:
unit = _configured_unit(unit, units)
meta["unit_of_measurement"] = unit
statistic_ids = {
@@ -277,7 +319,8 @@ def list_statistic_ids(
platform_statistic_ids = platform.list_statistic_ids(hass, statistic_type)
for statistic_id, unit in platform_statistic_ids.items():
unit = _configured_unit(unit, units)
if unit is not None:
unit = _configured_unit(unit, units)
platform_statistic_ids[statistic_id] = unit
statistic_ids = {**statistic_ids, **platform_statistic_ids}
@@ -367,7 +410,7 @@ def _sorted_statistics_to_dict(
hass: HomeAssistant,
stats: list,
statistic_ids: list[str] | None,
metadata: dict[str, dict[str, str]],
metadata: dict[str, StatisticMetaData],
) -> dict[str, list[dict]]:
"""Convert SQL results into JSON friendly data structure."""
result: dict = defaultdict(list)

View File

@@ -52,6 +52,7 @@ class RingCam(RingEntityMixin, Camera):
self._last_event = None
self._last_video_id = None
self._video_url = None
self._image = None
self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL
async def async_added_to_hass(self):
@@ -80,6 +81,7 @@ class RingCam(RingEntityMixin, Camera):
self._last_event = None
self._last_video_id = None
self._video_url = None
self._image = None
self._expires_at = dt_util.utcnow()
self.async_write_ha_state()
@@ -106,12 +108,18 @@ class RingCam(RingEntityMixin, Camera):
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
if self._video_url is None:
return
if self._image is None and self._video_url:
image = await ffmpeg.async_get_image(
self.hass,
self._video_url,
width=width,
height=height,
)
return await ffmpeg.async_get_image(
self.hass, self._video_url, width=width, height=height
)
if image:
self._image = image
return self._image
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
@@ -144,6 +152,9 @@ class RingCam(RingEntityMixin, Camera):
if self._last_video_id == self._last_event["id"] and utcnow <= self._expires_at:
return
if self._last_video_id != self._last_event["id"]:
self._image = None
try:
video_url = await self.hass.async_add_executor_job(
self._device.recording_url, self._last_event["id"]

View File

@@ -2,7 +2,7 @@
"domain": "ring",
"name": "Ring",
"documentation": "https://www.home-assistant.io/integrations/ring",
"requirements": ["ring_doorbell==0.6.2"],
"requirements": ["ring_doorbell==0.7.1"],
"dependencies": ["ffmpeg"],
"codeowners": ["@balloob"],
"config_flow": true,

View File

@@ -108,6 +108,7 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = {
}
# Keep track of entities for which a warning about decreasing value has been logged
SEEN_DIP = "sensor_seen_total_increasing_dip"
WARN_DIP = "sensor_warn_total_increasing_dip"
# Keep track of entities for which a warning about unsupported unit has been logged
WARN_UNSUPPORTED_UNIT = "sensor_warn_unsupported_unit"
@@ -233,7 +234,17 @@ def _normalize_states(
def warn_dip(hass: HomeAssistant, entity_id: str) -> None:
"""Log a warning once if a sensor with state_class_total has a decreasing value."""
"""Log a warning once if a sensor with state_class_total has a decreasing value.
The log will be suppressed until two dips have been seen to prevent warning due to
rounding issues with databases storing the state as a single precision float, which
was fixed in recorder DB version 20.
"""
if SEEN_DIP not in hass.data:
hass.data[SEEN_DIP] = set()
if entity_id not in hass.data[SEEN_DIP]:
hass.data[SEEN_DIP].add(entity_id)
return
if WARN_DIP not in hass.data:
hass.data[WARN_DIP] = set()
if entity_id not in hass.data[WARN_DIP]:
@@ -341,7 +352,7 @@ def compile_statistics(
# We have compiled history for this sensor before, use that as a starting point
last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"]
new_state = old_state = last_stats[entity_id][0]["state"]
_sum = last_stats[entity_id][0]["sum"]
_sum = last_stats[entity_id][0]["sum"] or 0
for fstate, state in fstates:

View File

@@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==11.0.4"],
"requirements": ["simplisafe-python==11.0.5"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling"
}

View File

@@ -561,7 +561,7 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity):
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return f"{self._device.device_id}.{self.report_name}"
return f"{self._device.device_id}.{self.report_name}_meter"
@property
def native_value(self):

View File

@@ -64,9 +64,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize the flow."""
self._reauth = False
self._entry_id = None
self._entry_data = {}
self.entry = None
@staticmethod
@callback
@@ -76,10 +74,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reauth(self, data: dict[str, Any] | None = None) -> FlowResult:
"""Handle configuration by re-auth."""
self._reauth = True
self._entry_data = dict(data)
entry = await self.async_set_unique_id(self.unique_id)
self._entry_id = entry.entry_id
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
@@ -90,7 +85,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={"host": self._entry_data[CONF_HOST]},
description_placeholders={"host": self.entry.data[CONF_HOST]},
data_schema=vol.Schema({}),
errors={},
)
@@ -104,8 +99,8 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is not None:
if self._reauth:
user_input = {**self._entry_data, **user_input}
if self.entry:
user_input = {**self.entry.data, **user_input}
if CONF_VERIFY_SSL not in user_input:
user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL
@@ -120,10 +115,8 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
else:
if self._reauth:
return await self._async_reauth_update_entry(
self._entry_id, user_input
)
if self.entry:
return await self._async_reauth_update_entry(user_input)
return self.async_create_entry(
title=user_input[CONF_HOST], data=user_input
@@ -136,17 +129,16 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def _async_reauth_update_entry(self, entry_id: str, data: dict) -> FlowResult:
async def _async_reauth_update_entry(self, data: dict) -> FlowResult:
"""Update existing config entry."""
entry = self.hass.config_entries.async_get_entry(entry_id)
self.hass.config_entries.async_update_entry(entry, data=data)
await self.hass.config_entries.async_reload(entry.entry_id)
self.hass.config_entries.async_update_entry(self.entry, data=data)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")
def _get_user_data_schema(self) -> dict[str, Any]:
"""Get the data schema to display user form."""
if self._reauth:
if self.entry:
return {vol.Required(CONF_API_KEY): str}
data_schema = {

View File

@@ -223,6 +223,7 @@ async def async_setup_entry(
{
vol.Required(ATTR_ALARM_ID): cv.positive_int,
vol.Optional(ATTR_TIME): cv.time,
vol.Optional(ATTR_VOLUME): cv.small_float,
vol.Optional(ATTR_ENABLED): cv.boolean,
vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean,
},

View File

@@ -323,6 +323,18 @@ class SonosSpeaker:
async def async_subscribe(self) -> bool:
"""Initiate event subscriptions."""
_LOGGER.debug("Creating subscriptions for %s", self.zone_name)
# Create a polling task in case subscriptions fail or callback events do not arrive
if not self._poll_timer:
self._poll_timer = self.hass.helpers.event.async_track_time_interval(
partial(
async_dispatcher_send,
self.hass,
f"{SONOS_POLL_UPDATE}-{self.soco.uid}",
),
SCAN_INTERVAL,
)
try:
await self.hass.async_add_executor_job(self.set_basic_info)
@@ -337,10 +349,10 @@ class SonosSpeaker:
for service in SUBSCRIPTION_SERVICES
]
await asyncio.gather(*subscriptions)
return True
except SoCoException as ex:
_LOGGER.warning("Could not connect %s: %s", self.zone_name, ex)
return False
return True
async def _subscribe(
self, target: SubscriptionBase, sub_callback: Callable
@@ -497,15 +509,6 @@ class SonosSpeaker:
self.soco.ip_address,
)
self._poll_timer = self.hass.helpers.event.async_track_time_interval(
partial(
async_dispatcher_send,
self.hass,
f"{SONOS_POLL_UPDATE}-{self.soco.uid}",
),
SCAN_INTERVAL,
)
if self._is_ready and not self.subscriptions_failed:
done = await self.async_subscribe()
if not done:
@@ -567,15 +570,6 @@ class SonosSpeaker:
self._seen_timer = self.hass.helpers.event.async_call_later(
SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen
)
if not self._poll_timer:
self._poll_timer = self.hass.helpers.event.async_track_time_interval(
partial(
async_dispatcher_send,
self.hass,
f"{SONOS_POLL_UPDATE}-{self.soco.uid}",
),
SCAN_INTERVAL,
)
self.async_write_entity_states()
#

View File

@@ -286,6 +286,11 @@ class Scanner:
if header_st is not None:
self.seen.add((header_st, header_location))
def _async_unsee(self, header_st: str | None, header_location: str | None) -> None:
"""If we see a device in a new location, unsee the original location."""
if header_st is not None:
self.seen.remove((header_st, header_location))
async def _async_process_entry(self, headers: Mapping[str, str]) -> None:
"""Process SSDP entries."""
_LOGGER.debug("_async_process_entry: %s", headers)
@@ -293,7 +298,12 @@ class Scanner:
h_location = headers.get("location")
if h_st and (udn := _udn_from_usn(headers.get("usn"))):
self.cache[(udn, h_st)] = headers
cache_key = (udn, h_st)
if old_headers := self.cache.get(cache_key):
old_h_location = old_headers.get("location")
if h_location != old_h_location:
self._async_unsee(old_headers.get("st"), old_h_location)
self.cache[cache_key] = headers
callbacks = self._async_get_matching_callbacks(headers)
if self._async_seen(h_st, h_location) and not callbacks:

View File

@@ -168,10 +168,7 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity):
return
if self.home_variable == "outdoor temperature":
self._state = self.hass.config.units.temperature(
self._tado_weather_data["outsideTemperature"]["celsius"],
TEMP_CELSIUS,
)
self._state = self._tado_weather_data["outsideTemperature"]["celsius"]
self._state_attributes = {
"time": self._tado_weather_data["outsideTemperature"]["timestamp"],
}
@@ -245,7 +242,7 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity):
def native_unit_of_measurement(self):
"""Return the unit of measurement."""
if self.zone_variable == "temperature":
return self.hass.config.units.temperature_unit
return TEMP_CELSIUS
if self.zone_variable == "humidity":
return PERCENTAGE
if self.zone_variable == "heating":
@@ -277,9 +274,7 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity):
return
if self.zone_variable == "temperature":
self._state = self.hass.config.units.temperature(
self._tado_zone_data.current_temp, TEMP_CELSIUS
)
self._state = self._tado_zone_data.current_temp
self._state_attributes = {
"time": self._tado_zone_data.current_temp_timestamp,
"setting": 0, # setting is used in climate device

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import dataclasses
import fnmatch
import logging
import os
import sys
@@ -72,6 +73,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
def _fnmatch_lower(name: str | None, pattern: str) -> bool:
"""Match a lowercase version of the name."""
if name is None:
return False
return fnmatch.fnmatch(name.lower(), pattern)
class USBDiscovery:
"""Manage USB Discovery."""
@@ -119,7 +127,13 @@ class USBDiscovery:
return
monitor = Monitor.from_netlink(context)
monitor.filter_by(subsystem="tty")
try:
monitor.filter_by(subsystem="tty")
except ValueError as ex: # this fails on WSL
_LOGGER.debug(
"Unable to setup pyudev filtering; This is expected on WSL: %s", ex
)
return
observer = MonitorObserver(
monitor, callback=self._device_discovered, name="usb-observer"
)
@@ -152,6 +166,18 @@ class USBDiscovery:
continue
if "pid" in matcher and device.pid != matcher["pid"]:
continue
if "serial_number" in matcher and not _fnmatch_lower(
device.serial_number, matcher["serial_number"]
):
continue
if "manufacturer" in matcher and not _fnmatch_lower(
device.manufacturer, matcher["manufacturer"]
):
continue
if "description" in matcher and not _fnmatch_lower(
device.description, matcher["description"]
):
continue
flow: USBFlow = {
"domain": matcher["domain"],
"context": {"source": config_entries.SOURCE_USB},

View File

@@ -87,7 +87,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
source = HaVersionSource.CONTAINER
if (
source in (HaVersionSource.SUPERVISOR, HaVersionSource.CONTAINER)
source == HaVersionSource.CONTAINER
and image is not None
and image != DEFAULT_IMAGE
):

View File

@@ -333,7 +333,7 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity):
}
)
self._mode = self._state_attrs.get(ATTR_MODE)
self._fan_level = self.coordinator.data.fan_level
self._fan_level = getattr(self.coordinator.data, ATTR_FAN_LEVEL, None)
self.async_write_ha_state()
#
@@ -440,7 +440,7 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
{attribute: None for attribute in self._available_attributes}
)
self._mode = self._state_attrs.get(ATTR_MODE)
self._fan_level = self.coordinator.data.fan_level
self._fan_level = getattr(self.coordinator.data, ATTR_FAN_LEVEL, None)
@property
def preset_mode(self):

View File

@@ -196,7 +196,6 @@ async def _async_initialize(
entry_data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id] = {
DATA_PLATFORMS_LOADED: False
}
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
@callback
def _async_load_platforms():
@@ -212,6 +211,15 @@ async def _async_initialize(
await device.async_setup()
entry_data[DATA_DEVICE] = device
if (
device.capabilities
and entry.options.get(CONF_MODEL) != device.capabilities["model"]
):
hass.config_entries.async_update_entry(
entry, options={**entry.options, CONF_MODEL: device.capabilities["model"]}
)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
entry.async_on_unload(
async_dispatcher_connect(
hass, DEVICE_INITIALIZED.format(host), _async_load_platforms
@@ -540,7 +548,7 @@ class YeelightDevice:
self._config = config
self._host = host
self._bulb_device = bulb
self._capabilities = {}
self.capabilities = {}
self._device_type = None
self._available = False
self._initialized = False
@@ -574,12 +582,12 @@ class YeelightDevice:
@property
def model(self):
"""Return configured/autodetected device model."""
return self._bulb_device.model or self._capabilities.get("model")
return self._bulb_device.model or self.capabilities.get("model")
@property
def fw_version(self):
"""Return the firmware version."""
return self._capabilities.get("fw_ver")
return self.capabilities.get("fw_ver")
@property
def is_nightlight_supported(self) -> bool:
@@ -674,20 +682,22 @@ class YeelightDevice:
async def async_setup(self):
"""Fetch capabilities and setup name if available."""
scanner = YeelightScanner.async_get(self._hass)
self._capabilities = await scanner.async_get_capabilities(self._host) or {}
self.capabilities = await scanner.async_get_capabilities(self._host) or {}
if self.capabilities:
self._bulb_device.set_capabilities(self.capabilities)
if name := self._config.get(CONF_NAME):
# Override default name when name is set in config
self._name = name
elif self._capabilities:
elif self.capabilities:
# Generate name from model and id when capabilities is available
self._name = _async_unique_name(self._capabilities)
self._name = _async_unique_name(self.capabilities)
else:
self._name = self._host # Default name is host
async def async_update(self):
async def async_update(self, force=False):
"""Update device properties and send data updated signal."""
if self._initialized and self._available:
# No need to poll, already connected
if not force and self._initialized and self._available:
# No need to poll unless force, already connected
return
await self._async_update_properties()
async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))

View File

@@ -96,7 +96,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
return self.async_create_entry(
title=async_format_model_id(self._discovered_model, self.unique_id),
data={CONF_ID: self.unique_id, CONF_HOST: self._discovered_ip},
data={
CONF_ID: self.unique_id,
CONF_HOST: self._discovered_ip,
CONF_MODEL: self._discovered_model,
},
)
self._set_confirm_only()
@@ -129,6 +133,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data={
CONF_HOST: user_input[CONF_HOST],
CONF_ID: self.unique_id,
CONF_MODEL: model,
},
)
@@ -151,7 +156,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
host = urlparse(capabilities["location"]).hostname
return self.async_create_entry(
title=_async_unique_name(capabilities),
data={CONF_ID: unique_id, CONF_HOST: host},
data={
CONF_ID: unique_id,
CONF_HOST: host,
CONF_MODEL: capabilities["model"],
},
)
configured_devices = {

View File

@@ -762,6 +762,10 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
_LOGGER.error("Unable to set the defaults: %s", ex)
return
# Some devices (mainly nightlights) will not send back the on state so we need to force a refresh
if not self.is_on:
await self.device.async_update(True)
async def async_turn_off(self, **kwargs) -> None:
"""Turn off."""
if not self.is_on:
@@ -772,6 +776,9 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
await self.device.async_turn_off(duration=duration, light_type=self.light_type)
# Some devices will not send back the off state so we need to force a refresh
if self.is_on:
await self.device.async_update(True)
async def async_set_mode(self, mode: str):
"""Set a power mode."""
@@ -852,7 +859,12 @@ class YeelightColorLightWithoutNightlightSwitch(
@property
def _brightness_property(self):
return "current_brightness"
# If the nightlight is not active, we do not
# want to "current_brightness" since it will check
# "bg_power" and main light could still be on
if self.device.is_nightlight_enabled:
return "current_brightness"
return super()._brightness_property
class YeelightColorLightWithNightlightSwitch(
@@ -876,7 +888,12 @@ class YeelightWhiteTempWithoutNightlightSwitch(
@property
def _brightness_property(self):
return "current_brightness"
# If the nightlight is not active, we do not
# want to "current_brightness" since it will check
# "bg_power" and main light could still be on
if self.device.is_nightlight_enabled:
return "current_brightness"
return super()._brightness_property
class YeelightWithNightLight(

View File

@@ -2,7 +2,7 @@
"domain": "yeelight",
"name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.7.3", "async-upnp-client==0.20.0"],
"requirements": ["yeelight==0.7.4", "async-upnp-client==0.20.0"],
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
"config_flow": true,
"dependencies": ["network"],

View File

@@ -25,6 +25,7 @@ SUPPORTED_PORT_SETTINGS = (
CONF_BAUDRATE,
CONF_FLOWCONTROL,
)
DECONZ_DOMAIN = "deconz"
class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -36,7 +37,6 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Initialize flow instance."""
self._device_path = None
self._radio_type = None
self._auto_detected_data = None
self._title = None
async def async_step_user(self, user_input=None):
@@ -121,18 +121,12 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# we ignore the usb discovery as they probably
# want to use it there instead
for flow in self.hass.config_entries.flow.async_progress():
if flow["handler"] == "deconz":
if flow["handler"] == DECONZ_DOMAIN:
return self.async_abort(reason="not_zha_device")
for entry in self.hass.config_entries.async_entries(DECONZ_DOMAIN):
if entry.source != config_entries.SOURCE_IGNORE:
return self.async_abort(reason="not_zha_device")
# The Nortek sticks are a special case since they
# have a Z-Wave and a Zigbee radio. We need to reject
# the Z-Wave radio.
if vid == "10C4" and pid == "8A2A" and "ZigBee" not in description:
return self.async_abort(reason="not_zha_device")
self._auto_detected_data = await detect_radios(dev_path)
if self._auto_detected_data is None:
return self.async_abort(reason="not_zha_device")
self._device_path = dev_path
self._title = usb.human_readable_device_name(
dev_path,
@@ -149,9 +143,15 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_confirm(self, user_input=None):
"""Confirm a discovery."""
if user_input is not None:
auto_detected_data = await detect_radios(self._device_path)
if auto_detected_data is None:
# This probably will not happen how they have
# have very specific usb matching, but there could
# be a problem with the device
return self.async_abort(reason="usb_probe_failed")
return self.async_create_entry(
title=self._title,
data=self._auto_detected_data,
data=auto_detected_data,
)
return self.async_show_form(

View File

@@ -4,22 +4,21 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
"bellows==0.26.0",
"bellows==0.27.0",
"pyserial==3.5",
"pyserial-asyncio==0.5",
"zha-quirks==0.0.59",
"zha-quirks==0.0.60",
"zigpy-cc==0.5.2",
"zigpy-deconz==0.12.1",
"zigpy==0.36.1",
"zigpy-xbee==0.13.0",
"zigpy-deconz==0.13.0",
"zigpy==0.37.1",
"zigpy-xbee==0.14.0",
"zigpy-zigate==0.7.3",
"zigpy-znp==0.5.3"
"zigpy-znp==0.5.4"
],
"usb": [
{"vid":"10C4","pid":"EA60","known_devices":["slae.sh cc2652rb stick"]},
{"vid":"1CF1","pid":"0030","known_devices":["Conbee II"]},
{"vid":"1A86","pid":"7523","known_devices":["Electrolama zig-a-zig-ah"]},
{"vid":"10C4","pid":"8A2A","known_devices":["Nortek HUSBZB-1"]}
{"vid":"10C4","pid":"EA60","description":"*2652*","known_devices":["slae.sh cc2652rb stick"]},
{"vid":"1CF1","pid":"0030","description":"*conbee*","known_devices":["Conbee II"]},
{"vid":"10C4","pid":"8A2A","description":"*zigbee*","known_devices":["Nortek HUSBZB-1"]}
],
"codeowners": ["@dmulcahey", "@adminiuga"],
"zeroconf": [

View File

@@ -30,7 +30,8 @@
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_zha_device": "This device is not a zha device"
"not_zha_device": "This device is not a zha device",
"usb_probe_failed": "Failed to probe the usb device"
}
},
"config_panel": {

View File

@@ -5,7 +5,7 @@ from typing import Final
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 9
PATCH_VERSION: Final = "0.dev0"
PATCH_VERSION: Final = "0b2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@@ -9,22 +9,20 @@ USB = [
{
"domain": "zha",
"vid": "10C4",
"pid": "EA60"
"pid": "EA60",
"description": "*2652*"
},
{
"domain": "zha",
"vid": "1CF1",
"pid": "0030"
},
{
"domain": "zha",
"vid": "1A86",
"pid": "7523"
"pid": "0030",
"description": "*conbee*"
},
{
"domain": "zha",
"vid": "10C4",
"pid": "8A2A"
"pid": "8A2A",
"description": "*zigbee*"
},
{
"domain": "zwave_js",

View File

@@ -201,7 +201,7 @@ aiokafka==0.6.0
aiokef==0.2.16
# homeassistant.components.lifx
aiolifx==0.6.9
aiolifx==0.6.10
# homeassistant.components.lifx
aiolifx_effects==0.2.2
@@ -237,7 +237,7 @@ aiopvpc==2.2.0
aiopylgtv==0.4.0
# homeassistant.components.recollect_waste
aiorecollect==1.0.7
aiorecollect==1.0.8
# homeassistant.components.shelly
aioshelly==0.6.4
@@ -372,10 +372,10 @@ beautifulsoup4==4.9.3
# beewi_smartclim==0.0.10
# homeassistant.components.zha
bellows==0.26.0
bellows==0.27.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.19
bimmer_connected==0.7.20
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@@ -2043,7 +2043,7 @@ rfk101py==0.0.1
rflink==0.0.58
# homeassistant.components.ring
ring_doorbell==0.6.2
ring_doorbell==0.7.1
# homeassistant.components.fleetgo
ritassist==0.9.2
@@ -2131,7 +2131,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==11.0.4
simplisafe-python==11.0.5
# homeassistant.components.sisyphus
sisyphus-control==3.0
@@ -2438,7 +2438,7 @@ yalesmartalarmclient==0.3.4
yalexs==1.1.13
# homeassistant.components.yeelight
yeelight==0.7.3
yeelight==0.7.4
# homeassistant.components.yeelightsunflower
yeelightsunflower==0.0.10
@@ -2459,7 +2459,7 @@ zengge==0.2
zeroconf==0.36.0
# homeassistant.components.zha
zha-quirks==0.0.59
zha-quirks==0.0.60
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -2471,19 +2471,19 @@ ziggo-mediabox-xl==1.1.0
zigpy-cc==0.5.2
# homeassistant.components.zha
zigpy-deconz==0.12.1
zigpy-deconz==0.13.0
# homeassistant.components.zha
zigpy-xbee==0.13.0
zigpy-xbee==0.14.0
# homeassistant.components.zha
zigpy-zigate==0.7.3
# homeassistant.components.zha
zigpy-znp==0.5.3
zigpy-znp==0.5.4
# homeassistant.components.zha
zigpy==0.36.1
zigpy==0.37.1
# homeassistant.components.zoneminder
zm-py==0.5.2

View File

@@ -158,7 +158,7 @@ aiopvpc==2.2.0
aiopylgtv==0.4.0
# homeassistant.components.recollect_waste
aiorecollect==1.0.7
aiorecollect==1.0.8
# homeassistant.components.shelly
aioshelly==0.6.4
@@ -227,10 +227,10 @@ azure-eventhub==5.5.0
base36==0.1.1
# homeassistant.components.zha
bellows==0.26.0
bellows==0.27.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.19
bimmer_connected==0.7.20
# homeassistant.components.blebox
blebox_uniapi==1.3.3
@@ -1148,7 +1148,7 @@ restrictedpython==5.1
rflink==0.0.58
# homeassistant.components.ring
ring_doorbell==0.6.2
ring_doorbell==0.7.1
# homeassistant.components.roku
rokuecp==0.8.1
@@ -1191,7 +1191,7 @@ sharkiqpy==0.1.8
simplehound==0.3
# homeassistant.components.simplisafe
simplisafe-python==11.0.4
simplisafe-python==11.0.5
# homeassistant.components.slack
slackclient==2.5.0
@@ -1367,7 +1367,7 @@ yalesmartalarmclient==0.3.4
yalexs==1.1.13
# homeassistant.components.yeelight
yeelight==0.7.3
yeelight==0.7.4
# homeassistant.components.youless
youless-api==0.12
@@ -1379,25 +1379,25 @@ zeep[async]==4.0.0
zeroconf==0.36.0
# homeassistant.components.zha
zha-quirks==0.0.59
zha-quirks==0.0.60
# homeassistant.components.zha
zigpy-cc==0.5.2
# homeassistant.components.zha
zigpy-deconz==0.12.1
zigpy-deconz==0.13.0
# homeassistant.components.zha
zigpy-xbee==0.13.0
zigpy-xbee==0.14.0
# homeassistant.components.zha
zigpy-zigate==0.7.3
# homeassistant.components.zha
zigpy-znp==0.5.3
zigpy-znp==0.5.4
# homeassistant.components.zha
zigpy==0.36.1
zigpy==0.37.1
# homeassistant.components.zwave_js
zwave-js-server-python==0.29.0

View File

@@ -210,6 +210,9 @@ MANIFEST_SCHEMA = vol.Schema(
{
vol.Optional("vid"): vol.All(str, verify_uppercase),
vol.Optional("pid"): vol.All(str, verify_uppercase),
vol.Optional("serial_number"): vol.All(str, verify_lowercase),
vol.Optional("manufacturer"): vol.All(str, verify_lowercase),
vol.Optional("description"): vol.All(str, verify_lowercase),
vol.Optional("known_devices"): [str],
}
)

View File

@@ -77,6 +77,28 @@ async def test_get_image_from_camera(hass, image_mock_url):
assert image.content == b"Test"
async def test_legacy_async_get_image_signature_warns_only_once(
hass, image_mock_url, caplog
):
"""Test that we only warn once when we encounter a legacy async_get_image function signature."""
async def _legacy_async_camera_image(self):
return b"Image"
with patch(
"homeassistant.components.demo.camera.DemoCamera.async_camera_image",
new=_legacy_async_camera_image,
):
image = await camera.async_get_image(hass, "camera.demo_camera")
assert image.content == b"Image"
assert "does not support requesting width and height" in caplog.text
caplog.clear()
image = await camera.async_get_image(hass, "camera.demo_camera")
assert image.content == b"Image"
assert "does not support requesting width and height" not in caplog.text
async def test_get_image_from_camera_with_width_height(hass, image_mock_url):
"""Grab an image from camera entity with width and height."""

View File

@@ -593,6 +593,7 @@ async def test_pymodbus_constructor_fail(hass, caplog):
config = {
DOMAIN: [
{
CONF_NAME: TEST_MODBUS_NAME,
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
@@ -606,7 +607,8 @@ async def test_pymodbus_constructor_fail(hass, caplog):
mock_pb.side_effect = ModbusException("test no class")
assert await async_setup_component(hass, DOMAIN, config) is False
await hass.async_block_till_done()
assert caplog.messages[0].startswith("Pymodbus: Modbus Error: test")
message = f"Pymodbus: {TEST_MODBUS_NAME}: Modbus Error: test"
assert caplog.messages[0].startswith(message)
assert caplog.records[0].levelname == "ERROR"
assert mock_pb.called

View File

@@ -7,6 +7,7 @@ import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import mqtt
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@@ -100,7 +101,7 @@ async def test_user_single_instance(hass):
assert result["reason"] == "single_instance_allowed"
async def test_hassio_single_instance(hass):
async def test_hassio_already_configured(hass):
"""Test we only allow a single config flow."""
MockConfigEntry(domain="mqtt").add_to_hass(hass)
@@ -108,7 +109,23 @@ async def test_hassio_single_instance(hass):
"mqtt", context={"source": config_entries.SOURCE_HASSIO}
)
assert result["type"] == "abort"
assert result["reason"] == "single_instance_allowed"
assert result["reason"] == "already_configured"
async def test_hassio_ignored(hass: HomeAssistant) -> None:
"""Test we supervisor discovered instance can be ignored."""
MockConfigEntry(
domain=mqtt.DOMAIN, source=config_entries.SOURCE_IGNORE
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
mqtt.DOMAIN,
data={"addon": "Mosquitto", "host": "mock-mosquitto", "port": "1883"},
context={"source": config_entries.SOURCE_HASSIO},
)
assert result
assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT
assert result.get("reason") == "already_configured"
async def test_hassio_confirm(hass, mock_try_connection, mock_finish_setup):

View File

@@ -10,6 +10,7 @@ from homeassistant.components.recorder import history
from homeassistant.components.recorder.const import DATA_INSTANCE
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
from homeassistant.components.recorder.statistics import (
get_metadata,
list_statistic_ids,
statistics_during_period,
)
@@ -371,7 +372,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip(
"state_class": "total_increasing",
"unit_of_measurement": unit,
}
seq = [10, 15, 20, 19, 30, 40, 50, 60, 70]
seq = [10, 15, 20, 19, 30, 40, 39, 60, 70]
four, eight, states = record_meter_states(
hass, zero, "sensor.test1", attributes, seq
@@ -385,8 +386,20 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip(
wait_recording_done(hass)
recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1))
wait_recording_done(hass)
assert (
"Entity sensor.test1 has state class total_increasing, but its state is not "
"strictly increasing. Please create a bug report at https://github.com/"
"home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A"
"+recorder%22"
) not in caplog.text
recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2))
wait_recording_done(hass)
assert (
"Entity sensor.test1 has state class total_increasing, but its state is not "
"strictly increasing. Please create a bug report at https://github.com/"
"home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A"
"+recorder%22"
) in caplog.text
statistic_ids = list_statistic_ids(hass)
assert statistic_ids == [
{"statistic_id": "sensor.test1", "unit_of_measurement": native_unit}
@@ -427,12 +440,6 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip(
]
}
assert "Error while processing event StatisticsTask" not in caplog.text
assert (
"Entity sensor.test1 has state class total_increasing, but its state is not "
"strictly increasing. Please create a bug report at https://github.com/"
"home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A"
"+recorder%22"
) in caplog.text
def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog):
@@ -1031,6 +1038,95 @@ def test_compile_hourly_statistics_changing_units_2(
assert "Error while processing event StatisticsTask" not in caplog.text
@pytest.mark.parametrize(
"device_class,unit,native_unit,mean,min,max",
[
(None, None, None, 16.440677, 10, 30),
],
)
def test_compile_hourly_statistics_changing_statistics(
hass_recorder, caplog, device_class, unit, native_unit, mean, min, max
):
"""Test compiling hourly statistics where units change during an hour."""
zero = dt_util.utcnow()
hass = hass_recorder()
recorder = hass.data[DATA_INSTANCE]
setup_component(hass, "sensor", {})
attributes_1 = {
"device_class": device_class,
"state_class": "measurement",
"unit_of_measurement": unit,
}
attributes_2 = {
"device_class": device_class,
"state_class": "total_increasing",
"unit_of_measurement": unit,
}
four, states = record_states(hass, zero, "sensor.test1", attributes_1)
recorder.do_adhoc_statistics(period="hourly", start=zero)
wait_recording_done(hass)
statistic_ids = list_statistic_ids(hass)
assert statistic_ids == [
{"statistic_id": "sensor.test1", "unit_of_measurement": None}
]
metadata = get_metadata(hass, "sensor.test1")
assert metadata == {
"has_mean": True,
"has_sum": False,
"statistic_id": "sensor.test1",
"unit_of_measurement": None,
}
# Add more states, with changed state class
four, _states = record_states(
hass, zero + timedelta(hours=1), "sensor.test1", attributes_2
)
states["sensor.test1"] += _states["sensor.test1"]
hist = history.get_significant_states(hass, zero, four)
assert dict(states) == dict(hist)
recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1))
wait_recording_done(hass)
statistic_ids = list_statistic_ids(hass)
assert statistic_ids == [
{"statistic_id": "sensor.test1", "unit_of_measurement": None}
]
metadata = get_metadata(hass, "sensor.test1")
assert metadata == {
"has_mean": False,
"has_sum": True,
"statistic_id": "sensor.test1",
"unit_of_measurement": None,
}
stats = statistics_during_period(hass, zero)
assert stats == {
"sensor.test1": [
{
"statistic_id": "sensor.test1",
"start": process_timestamp_to_utc_isoformat(zero),
"mean": approx(mean),
"min": approx(min),
"max": approx(max),
"last_reset": None,
"state": None,
"sum": None,
},
{
"statistic_id": "sensor.test1",
"start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)),
"mean": None,
"min": None,
"max": None,
"last_reset": None,
"state": approx(30.0),
"sum": approx(30.0),
},
]
}
assert "Error while processing event StatisticsTask" not in caplog.text
def record_states(hass, zero, entity_id, attributes):
"""Record some test states.

View File

@@ -168,7 +168,7 @@ async def test_power_consumption_sensor(hass, device_factory):
assert state.state == "1412.002"
entry = entity_registry.async_get("sensor.refrigerator_energy")
assert entry
assert entry.unique_id == f"{device.device_id}.energy"
assert entry.unique_id == f"{device.device_id}.energy_meter"
entry = device_registry.async_get_device({(DOMAIN, device.device_id)})
assert entry
assert entry.name == device.label
@@ -180,7 +180,7 @@ async def test_power_consumption_sensor(hass, device_factory):
assert state.state == "109"
entry = entity_registry.async_get("sensor.refrigerator_power")
assert entry
assert entry.unique_id == f"{device.device_id}.power"
assert entry.unique_id == f"{device.device_id}.power_meter"
entry = device_registry.async_get_device({(DOMAIN, device.device_id)})
assert entry
assert entry.name == device.label
@@ -202,7 +202,7 @@ async def test_power_consumption_sensor(hass, device_factory):
assert state.state == "unknown"
entry = entity_registry.async_get("sensor.vacuum_energy")
assert entry
assert entry.unique_id == f"{device.device_id}.energy"
assert entry.unique_id == f"{device.device_id}.energy_meter"
entry = device_registry.async_get_device({(DOMAIN, device.device_id)})
assert entry
assert entry.name == device.label

View File

@@ -100,14 +100,16 @@ async def test_full_reauth_flow_implementation(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the manual reauth flow from start to finish."""
entry = await setup_integration(
hass, aioclient_mock, skip_entry_setup=True, unique_id="any"
)
entry = await setup_integration(hass, aioclient_mock, skip_entry_setup=True)
assert entry
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_REAUTH, "unique_id": entry.unique_id},
context={
CONF_SOURCE: SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
data=entry.data,
)

View File

@@ -926,3 +926,127 @@ async def test_ipv4_does_additional_search_for_sonos(hass, caplog):
),
),
}
async def test_location_change_evicts_prior_location_from_cache(hass, aioclient_mock):
"""Test that a location change for a UDN will evict the prior location from the cache."""
mock_get_ssdp = {
"hue": [{"manufacturer": "Signify", "modelName": "Philips hue bridge 2015"}]
}
hue_response = """
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://{ip_address}:80/</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
<friendlyName>Philips hue ({ip_address})</friendlyName>
<manufacturer>Signify</manufacturer>
<manufacturerURL>http://www.philips-hue.com</manufacturerURL>
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
<modelName>Philips hue bridge 2015</modelName>
<modelNumber>BSB002</modelNumber>
<modelURL>http://www.philips-hue.com</modelURL>
<serialNumber>001788a36abf</serialNumber>
<UDN>uuid:2f402f80-da50-11e1-9b23-001788a36abf</UDN>
</device>
</root>
"""
aioclient_mock.get(
"http://192.168.212.23/description.xml",
text=hue_response.format(ip_address="192.168.212.23"),
)
aioclient_mock.get(
"http://169.254.8.155/description.xml",
text=hue_response.format(ip_address="169.254.8.155"),
)
ssdp_response_without_location = {
"ST": "uuid:2f402f80-da50-11e1-9b23-001788a36abf",
"_udn": "uuid:2f402f80-da50-11e1-9b23-001788a36abf",
"USN": "uuid:2f402f80-da50-11e1-9b23-001788a36abf",
"SERVER": "Hue/1.0 UPnP/1.0 IpBridge/1.44.0",
"hue-bridgeid": "001788FFFEA36ABF",
"EXT": "",
}
mock_good_ip_ssdp_response = CaseInsensitiveDict(
**ssdp_response_without_location,
**{"LOCATION": "http://192.168.212.23/description.xml"},
)
mock_link_local_ip_ssdp_response = CaseInsensitiveDict(
**ssdp_response_without_location,
**{"LOCATION": "http://169.254.8.155/description.xml"},
)
mock_ssdp_response = mock_good_ip_ssdp_response
def _generate_fake_ssdp_listener(*args, **kwargs):
listener = SSDPListener(*args, **kwargs)
async def _async_callback(*_):
pass
@callback
def _callback(*_):
import pprint
pprint.pprint(mock_ssdp_response)
hass.async_create_task(listener.async_callback(mock_ssdp_response))
listener.async_start = _async_callback
listener.async_search = _callback
return listener
with patch(
"homeassistant.components.ssdp.async_get_ssdp",
return_value=mock_get_ssdp,
), patch(
"homeassistant.components.ssdp.SSDPListener",
new=_generate_fake_ssdp_listener,
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_init:
assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
await hass.async_block_till_done()
assert len(mock_init.mock_calls) == 1
assert mock_init.mock_calls[0][1][0] == "hue"
assert mock_init.mock_calls[0][2]["context"] == {
"source": config_entries.SOURCE_SSDP
}
assert (
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
== mock_good_ip_ssdp_response["location"]
)
mock_init.reset_mock()
mock_ssdp_response = mock_link_local_ip_ssdp_response
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=400))
await hass.async_block_till_done()
assert mock_init.mock_calls[0][1][0] == "hue"
assert mock_init.mock_calls[0][2]["context"] == {
"source": config_entries.SOURCE_SSDP
}
assert (
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
== mock_link_local_ip_ssdp_response["location"]
)
mock_init.reset_mock()
mock_ssdp_response = mock_good_ip_ssdp_response
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=600))
await hass.async_block_till_done()
assert mock_init.mock_calls[0][1][0] == "hue"
assert mock_init.mock_calls[0][2]["context"] == {
"source": config_entries.SOURCE_SSDP
}
assert (
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
== mock_good_ip_ssdp_response["location"]
)

View File

@@ -9,7 +9,7 @@ from homeassistant.components import usb
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.setup import async_setup_component
from . import slae_sh_device
from . import conbee_device, slae_sh_device
@pytest.fixture(name="operating_system")
@@ -38,6 +38,20 @@ def mock_docker():
yield
@pytest.fixture(name="venv")
def mock_venv():
"""Mock running Home Assistant in a venv container."""
with patch(
"homeassistant.components.usb.system_info.async_get_system_info",
return_value={
"hassio": False,
"docker": False,
"virtualenv": True,
},
):
yield
@pytest.mark.skipif(
not sys.platform.startswith("linux"),
reason="Only works on linux",
@@ -171,6 +185,297 @@ async def test_discovered_by_websocket_scan(hass, hass_ws_client):
assert mock_config_flow.mock_calls[0][1][0] == "test1"
async def test_discovered_by_websocket_scan_limited_by_description_matcher(
hass, hass_ws_client
):
"""Test a device is discovered from websocket scan is limited by the description matcher."""
new_usb = [
{"domain": "test1", "vid": "3039", "pid": "3039", "description": "*2652*"}
]
mock_comports = [
MagicMock(
device=slae_sh_device.device,
vid=12345,
pid=12345,
serial_number=slae_sh_device.serial_number,
manufacturer=slae_sh_device.manufacturer,
description=slae_sh_device.description,
)
]
with patch("pyudev.Context", side_effect=ImportError), patch(
"homeassistant.components.usb.async_get_usb", return_value=new_usb
), patch(
"homeassistant.components.usb.comports", return_value=mock_comports
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow:
assert await async_setup_component(hass, "usb", {"usb": {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "usb/scan"})
response = await ws_client.receive_json()
assert response["success"]
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "test1"
async def test_discovered_by_websocket_scan_rejected_by_description_matcher(
hass, hass_ws_client
):
"""Test a device is discovered from websocket scan rejected by the description matcher."""
new_usb = [
{"domain": "test1", "vid": "3039", "pid": "3039", "description": "*not_it*"}
]
mock_comports = [
MagicMock(
device=slae_sh_device.device,
vid=12345,
pid=12345,
serial_number=slae_sh_device.serial_number,
manufacturer=slae_sh_device.manufacturer,
description=slae_sh_device.description,
)
]
with patch("pyudev.Context", side_effect=ImportError), patch(
"homeassistant.components.usb.async_get_usb", return_value=new_usb
), patch(
"homeassistant.components.usb.comports", return_value=mock_comports
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow:
assert await async_setup_component(hass, "usb", {"usb": {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "usb/scan"})
response = await ws_client.receive_json()
assert response["success"]
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
async def test_discovered_by_websocket_scan_limited_by_serial_number_matcher(
hass, hass_ws_client
):
"""Test a device is discovered from websocket scan is limited by the serial_number matcher."""
new_usb = [
{
"domain": "test1",
"vid": "3039",
"pid": "3039",
"serial_number": "00_12_4b_00*",
}
]
mock_comports = [
MagicMock(
device=slae_sh_device.device,
vid=12345,
pid=12345,
serial_number=slae_sh_device.serial_number,
manufacturer=slae_sh_device.manufacturer,
description=slae_sh_device.description,
)
]
with patch("pyudev.Context", side_effect=ImportError), patch(
"homeassistant.components.usb.async_get_usb", return_value=new_usb
), patch(
"homeassistant.components.usb.comports", return_value=mock_comports
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow:
assert await async_setup_component(hass, "usb", {"usb": {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "usb/scan"})
response = await ws_client.receive_json()
assert response["success"]
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "test1"
async def test_discovered_by_websocket_scan_rejected_by_serial_number_matcher(
hass, hass_ws_client
):
"""Test a device is discovered from websocket scan is rejected by the serial_number matcher."""
new_usb = [
{"domain": "test1", "vid": "3039", "pid": "3039", "serial_number": "123*"}
]
mock_comports = [
MagicMock(
device=slae_sh_device.device,
vid=12345,
pid=12345,
serial_number=slae_sh_device.serial_number,
manufacturer=slae_sh_device.manufacturer,
description=slae_sh_device.description,
)
]
with patch("pyudev.Context", side_effect=ImportError), patch(
"homeassistant.components.usb.async_get_usb", return_value=new_usb
), patch(
"homeassistant.components.usb.comports", return_value=mock_comports
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow:
assert await async_setup_component(hass, "usb", {"usb": {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "usb/scan"})
response = await ws_client.receive_json()
assert response["success"]
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
async def test_discovered_by_websocket_scan_limited_by_manufacturer_matcher(
hass, hass_ws_client
):
"""Test a device is discovered from websocket scan is limited by the manufacturer matcher."""
new_usb = [
{
"domain": "test1",
"vid": "3039",
"pid": "3039",
"manufacturer": "dresden elektronik ingenieurtechnik*",
}
]
mock_comports = [
MagicMock(
device=conbee_device.device,
vid=12345,
pid=12345,
serial_number=conbee_device.serial_number,
manufacturer=conbee_device.manufacturer,
description=conbee_device.description,
)
]
with patch("pyudev.Context", side_effect=ImportError), patch(
"homeassistant.components.usb.async_get_usb", return_value=new_usb
), patch(
"homeassistant.components.usb.comports", return_value=mock_comports
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow:
assert await async_setup_component(hass, "usb", {"usb": {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "usb/scan"})
response = await ws_client.receive_json()
assert response["success"]
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "test1"
async def test_discovered_by_websocket_scan_rejected_by_manufacturer_matcher(
hass, hass_ws_client
):
"""Test a device is discovered from websocket scan is rejected by the manufacturer matcher."""
new_usb = [
{
"domain": "test1",
"vid": "3039",
"pid": "3039",
"manufacturer": "other vendor*",
}
]
mock_comports = [
MagicMock(
device=conbee_device.device,
vid=12345,
pid=12345,
serial_number=conbee_device.serial_number,
manufacturer=conbee_device.manufacturer,
description=conbee_device.description,
)
]
with patch("pyudev.Context", side_effect=ImportError), patch(
"homeassistant.components.usb.async_get_usb", return_value=new_usb
), patch(
"homeassistant.components.usb.comports", return_value=mock_comports
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow:
assert await async_setup_component(hass, "usb", {"usb": {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "usb/scan"})
response = await ws_client.receive_json()
assert response["success"]
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
async def test_discovered_by_websocket_rejected_with_empty_serial_number_only(
hass, hass_ws_client
):
"""Test a device is discovered from websocket is rejected with empty serial number."""
new_usb = [
{"domain": "test1", "vid": "3039", "pid": "3039", "serial_number": "123*"}
]
mock_comports = [
MagicMock(
device=conbee_device.device,
vid=12345,
pid=12345,
serial_number=None,
manufacturer=None,
description=None,
)
]
with patch("pyudev.Context", side_effect=ImportError), patch(
"homeassistant.components.usb.async_get_usb", return_value=new_usb
), patch(
"homeassistant.components.usb.comports", return_value=mock_comports
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow:
assert await async_setup_component(hass, "usb", {"usb": {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "usb/scan"})
response = await ws_client.receive_json()
assert response["success"]
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
async def test_discovered_by_websocket_scan_match_vid_only(hass, hass_ws_client):
"""Test a device is discovered from websocket scan only matching vid."""
new_usb = [{"domain": "test1", "vid": "3039"}]
@@ -315,6 +620,48 @@ async def test_non_matching_discovered_by_scanner_after_started(
assert len(mock_config_flow.mock_calls) == 0
@pytest.mark.skipif(
not sys.platform.startswith("linux"),
reason="Only works on linux",
)
async def test_observer_on_wsl_fallback_without_throwing_exception(
hass, hass_ws_client, venv
):
"""Test that observer on WSL failure results in fallback to scanning without raising an exception."""
new_usb = [{"domain": "test1", "vid": "3039"}]
mock_comports = [
MagicMock(
device=slae_sh_device.device,
vid=12345,
pid=12345,
serial_number=slae_sh_device.serial_number,
manufacturer=slae_sh_device.manufacturer,
description=slae_sh_device.description,
)
]
with patch("pyudev.Context"), patch(
"pyudev.Monitor.filter_by", side_effect=ValueError
), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch(
"homeassistant.components.usb.comports", return_value=mock_comports
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow:
assert await async_setup_component(hass, "usb", {"usb": {}})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "usb/scan"})
response = await ws_client.receive_json()
assert response["success"]
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "test1"
@pytest.mark.skipif(
not sys.platform.startswith("linux"),
reason="Only works on linux",

View File

@@ -98,3 +98,33 @@ async def test_update(hass):
state = hass.states.get("sensor.current_version")
assert state
assert state.state == "1234"
async def test_image_name_container(hass):
"""Test the Version sensor with image name for container."""
config = {
"sensor": {"platform": "version", "source": "docker", "image": "qemux86-64"}
}
with patch("homeassistant.components.version.sensor.HaVersion") as haversion:
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
constructor = haversion.call_args[1]
assert constructor["source"] == "container"
assert constructor["image"] == "qemux86-64-homeassistant"
async def test_image_name_supervisor(hass):
"""Test the Version sensor with image name for supervisor."""
config = {
"sensor": {"platform": "version", "source": "hassio", "image": "qemux86-64"}
}
with patch("homeassistant.components.version.sensor.HaVersion") as haversion:
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
constructor = haversion.call_args[1]
assert constructor["source"] == "supervisor"
assert constructor["image"] == "qemux86-64"

View File

@@ -19,7 +19,7 @@ from homeassistant.components.yeelight import (
DOMAIN,
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
)
from homeassistant.components.yeelight.config_flow import CannotConnect
from homeassistant.components.yeelight.config_flow import MODEL_UNKNOWN, CannotConnect
from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM
@@ -28,6 +28,7 @@ from . import (
CAPABILITIES,
ID,
IP_ADDRESS,
MODEL,
MODULE,
MODULE_CONFIG_FLOW,
NAME,
@@ -87,7 +88,7 @@ async def test_discovery(hass: HomeAssistant):
)
assert result3["type"] == "create_entry"
assert result3["title"] == UNIQUE_FRIENDLY_NAME
assert result3["data"] == {CONF_ID: ID, CONF_HOST: IP_ADDRESS}
assert result3["data"] == {CONF_ID: ID, CONF_HOST: IP_ADDRESS, CONF_MODEL: MODEL}
await hass.async_block_till_done()
mock_setup.assert_called_once()
mock_setup_entry.assert_called_once()
@@ -160,7 +161,11 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant):
)
assert result3["type"] == "create_entry"
assert result3["title"] == UNIQUE_FRIENDLY_NAME
assert result3["data"] == {CONF_ID: ID, CONF_HOST: IP_ADDRESS}
assert result3["data"] == {
CONF_ID: ID,
CONF_HOST: IP_ADDRESS,
CONF_MODEL: MODEL,
}
await hass.async_block_till_done()
await hass.async_block_till_done()
@@ -300,7 +305,11 @@ async def test_manual(hass: HomeAssistant):
await hass.async_block_till_done()
assert result4["type"] == "create_entry"
assert result4["title"] == "Color 0x15243f"
assert result4["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f"}
assert result4["data"] == {
CONF_HOST: IP_ADDRESS,
CONF_ID: "0x000000000015243f",
CONF_MODEL: MODEL,
}
# Duplicate
result = await hass.config_entries.flow.async_init(
@@ -333,7 +342,7 @@ async def test_options(hass: HomeAssistant):
config = {
CONF_NAME: NAME,
CONF_MODEL: "",
CONF_MODEL: MODEL,
CONF_TRANSITION: DEFAULT_TRANSITION,
CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC,
CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE,
@@ -383,7 +392,11 @@ async def test_manual_no_capabilities(hass: HomeAssistant):
result["flow_id"], {CONF_HOST: IP_ADDRESS}
)
assert result["type"] == "create_entry"
assert result["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: None}
assert result["data"] == {
CONF_HOST: IP_ADDRESS,
CONF_ID: None,
CONF_MODEL: MODEL_UNKNOWN,
}
async def test_discovered_by_homekit_and_dhcp(hass):
@@ -480,7 +493,11 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data):
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f"}
assert result2["data"] == {
CONF_HOST: IP_ADDRESS,
CONF_ID: "0x000000000015243f",
CONF_MODEL: MODEL,
}
assert mock_async_setup.called
assert mock_async_setup_entry.called
@@ -540,7 +557,11 @@ async def test_discovered_ssdp(hass):
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f"}
assert result2["data"] == {
CONF_HOST: IP_ADDRESS,
CONF_ID: "0x000000000015243f",
CONF_MODEL: MODEL,
}
assert mock_async_setup.called
assert mock_async_setup_entry.called

View File

@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch
from yeelight import BulbException, BulbType
from homeassistant.components.yeelight import (
CONF_MODEL,
CONF_NIGHTLIGHT_SWITCH,
CONF_NIGHTLIGHT_SWITCH_TYPE,
DATA_CONFIG_ENTRIES,
@@ -35,6 +36,7 @@ from . import (
FAIL_TO_BIND_IP,
ID,
IP_ADDRESS,
MODEL,
MODULE,
SHORT_ID,
_mocked_bulb,
@@ -360,6 +362,7 @@ async def test_async_listen_error_late_discovery(hass, caplog):
assert "Failed to connect to bulb at" not in caplog.text
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.options[CONF_MODEL] == MODEL
async def test_async_listen_error_has_host_with_id(hass: HomeAssistant):

View File

@@ -96,6 +96,7 @@ from homeassistant.util.color import (
)
from . import (
CAPABILITIES,
ENTITY_LIGHT,
ENTITY_NIGHTLIGHT,
IP_ADDRESS,
@@ -1122,3 +1123,94 @@ async def test_effects(hass: HomeAssistant):
for name, target in effects.items():
await _async_test_effect(name, target)
await _async_test_effect("not_existed", called=False)
async def test_state_fails_to_update_triggers_update(hass: HomeAssistant):
"""Ensure we call async_get_properties if the turn on/off fails to update the state."""
mocked_bulb = _mocked_bulb()
properties = {**PROPERTIES}
properties.pop("active_mode")
properties["color_mode"] = "3" # HSV
mocked_bulb.last_properties = properties
mocked_bulb.bulb_type = BulbType.Color
config_entry = MockConfigEntry(
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
)
config_entry.add_to_hass(hass)
with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# We use asyncio.create_task now to avoid
# blocking starting so we need to block again
await hass.async_block_till_done()
mocked_bulb.last_properties["power"] = "off"
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_on.mock_calls) == 1
assert len(mocked_bulb.async_get_properties.mock_calls) == 2
mocked_bulb.last_properties["power"] = "on"
await hass.services.async_call(
"light",
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_off.mock_calls) == 1
assert len(mocked_bulb.async_get_properties.mock_calls) == 3
# But if the state is correct no calls
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_on.mock_calls) == 1
assert len(mocked_bulb.async_get_properties.mock_calls) == 3
async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant):
"""Test that main light on ambilights with the nightlight disabled shows the correct brightness."""
mocked_bulb = _mocked_bulb()
properties = {**PROPERTIES}
capabilities = {**CAPABILITIES}
capabilities["model"] = "ceiling10"
properties["color_mode"] = "3" # HSV
properties["bg_power"] = "off"
properties["current_brightness"] = 0
properties["bg_lmode"] = "2" # CT
mocked_bulb.last_properties = properties
mocked_bulb.bulb_type = BulbType.WhiteTempMood
main_light_entity_id = "light.yeelight_ceiling10_0x15243f"
config_entry = MockConfigEntry(
domain=DOMAIN,
data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False},
options={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False},
)
config_entry.add_to_hass(hass)
with _patch_discovery(capabilities=capabilities), patch(
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# We use asyncio.create_task now to avoid
# blocking starting so we need to block again
await hass.async_block_till_done()
state = hass.states.get(main_light_entity_id)
assert state.state == "on"
# bg_power off should not set the brightness to 0
assert state.attributes[ATTR_BRIGHTNESS] == 128

View File

@@ -7,7 +7,7 @@ import serial.tools.list_ports
import zigpy.config
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
from homeassistant import setup
from homeassistant import config_entries, setup
from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION,
ATTR_UPNP_MANUFACTURER_URL,
@@ -164,27 +164,17 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass):
"zha", context={"source": SOURCE_USB}, data=discovery_info
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "not_zha_device"
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm"
with patch("homeassistant.components.zha.async_setup_entry"):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
await hass.async_block_till_done()
@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
async def test_discovery_via_usb_rejects_nortek_zwave(detect_mock, hass):
"""Test usb flow -- reject the nortek zwave radio."""
discovery_info = {
"device": "/dev/null",
"vid": "10C4",
"pid": "8A2A",
"serial_number": "612020FD",
"description": "HubZ Smart Home Controller - HubZ Z-Wave Com Port",
"manufacturer": "Silicon Labs",
}
result = await hass.config_entries.flow.async_init(
"zha", context={"source": SOURCE_USB}, data=discovery_info
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "not_zha_device"
assert result2["type"] == RESULT_TYPE_ABORT
assert result2["reason"] == "usb_probe_failed"
@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
@@ -281,6 +271,52 @@ async def test_discovery_via_usb_deconz_already_discovered(detect_mock, hass):
assert result["reason"] == "not_zha_device"
@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
async def test_discovery_via_usb_deconz_already_setup(detect_mock, hass):
"""Test usb flow -- deconz setup."""
MockConfigEntry(domain="deconz", data={}).add_to_hass(hass)
await hass.async_block_till_done()
discovery_info = {
"device": "/dev/ttyZIGBEE",
"pid": "AAAA",
"vid": "AAAA",
"serial_number": "1234",
"description": "zigbee radio",
"manufacturer": "test",
}
result = await hass.config_entries.flow.async_init(
"zha", context={"source": SOURCE_USB}, data=discovery_info
)
await hass.async_block_till_done()
assert result["type"] == "abort"
assert result["reason"] == "not_zha_device"
@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
async def test_discovery_via_usb_deconz_ignored(detect_mock, hass):
"""Test usb flow -- deconz ignored."""
MockConfigEntry(
domain="deconz", source=config_entries.SOURCE_IGNORE, data={}
).add_to_hass(hass)
await hass.async_block_till_done()
discovery_info = {
"device": "/dev/ttyZIGBEE",
"pid": "AAAA",
"vid": "AAAA",
"serial_number": "1234",
"description": "zigbee radio",
"manufacturer": "test",
}
result = await hass.config_entries.flow.async_init(
"zha", context={"source": SOURCE_USB}, data=discovery_info
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm"
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
async def test_discovery_already_setup(detect_mock, hass):