This commit is contained in:
Franck Nijhof 2023-08-11 17:59:37 +02:00 committed by GitHub
commit 73898daff3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 607 additions and 166 deletions

View File

@ -8,5 +8,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyairvisual", "pysmb"], "loggers": ["pyairvisual", "pysmb"],
"requirements": ["pyairvisual==2022.12.1"] "requirements": ["pyairvisual==2023.08.1"]
} }

View File

@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyairvisual", "pysmb"], "loggers": ["pyairvisual", "pysmb"],
"requirements": ["pyairvisual==2022.12.1"] "requirements": ["pyairvisual==2023.08.1"]
} }

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.6.4"] "requirements": ["aioairzone==0.6.5"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aladdin_connect"], "loggers": ["aladdin_connect"],
"requirements": ["AIOAladdinConnect==0.1.56"] "requirements": ["AIOAladdinConnect==0.1.57"]
} }

View File

@ -26,6 +26,7 @@ from homeassistant.const import (
STATE_ON, STATE_ON,
) )
from homeassistant.core import HassJob, HomeAssistant from homeassistant.core import HassJob, HomeAssistant
from homeassistant.exceptions import ServiceNotFound
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
@ -293,9 +294,15 @@ class Alert(Entity):
LOGGER.debug(msg_payload) LOGGER.debug(msg_payload)
for target in self._notifiers: for target in self._notifiers:
await self.hass.services.async_call( try:
DOMAIN_NOTIFY, target, msg_payload, context=self._context await self.hass.services.async_call(
) DOMAIN_NOTIFY, target, msg_payload, context=self._context
)
except ServiceNotFound:
LOGGER.error(
"Failed to call notify.%s, retrying at next notification interval",
target,
)
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Async Unacknowledge alert.""" """Async Unacknowledge alert."""

View File

@ -58,6 +58,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
assert self.host assert self.host
api = create_api(self.hass, self.host, enable_ime=False) api = create_api(self.hass, self.host, enable_ime=False)
try: try:
await api.async_generate_cert_if_missing()
self.name, self.mac = await api.async_get_name_and_mac() self.name, self.mac = await api.async_get_name_and_mac()
assert self.mac assert self.mac
await self.async_set_unique_id(format_mac(self.mac)) await self.async_set_unique_id(format_mac(self.mac))

View File

@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv", "documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyatv", "srptools"], "loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.13.3"], "requirements": ["pyatv==0.13.4"],
"zeroconf": [ "zeroconf": [
"_mediaremotetv._tcp.local.", "_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.", "_companion-link._tcp.local.",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["bimmer_connected"], "loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected==0.13.8"] "requirements": ["bimmer-connected==0.13.9"]
} }

View File

@ -45,6 +45,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity):
"""Representation of a Broadlink light.""" """Representation of a Broadlink light."""
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None
def __init__(self, device): def __init__(self, device):
"""Initialize the light.""" """Initialize the light."""

View File

@ -207,7 +207,8 @@ class CommandSensor(ManualTriggerEntity, SensorEntity):
self._process_manual_data(value) self._process_manual_data(value)
return return
if self._value_template is not None: self._attr_native_value = None
if self._value_template is not None and value is not None:
value = self._value_template.async_render_with_possible_json_value( value = self._value_template.async_render_with_possible_json_value(
value, value,
None, None,
@ -221,7 +222,6 @@ class CommandSensor(ManualTriggerEntity, SensorEntity):
self._process_manual_data(value) self._process_manual_data(value)
return return
self._attr_native_value = None
if value is not None: if value is not None:
self._attr_native_value = async_parse_date_datetime( self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class value, self.entity_id, self.device_class

View File

@ -160,6 +160,15 @@ HostAttributes = TypedDict(
) )
class HostInfo(TypedDict):
"""FRITZ!Box host info class."""
mac: str
name: str
ip: str
status: bool
class UpdateCoordinatorDataType(TypedDict): class UpdateCoordinatorDataType(TypedDict):
"""Update coordinator data type.""" """Update coordinator data type."""
@ -380,16 +389,86 @@ class FritzBoxTools(
"""Event specific per FRITZ!Box entry to signal updates in devices.""" """Event specific per FRITZ!Box entry to signal updates in devices."""
return f"{DOMAIN}-device-update-{self._unique_id}" return f"{DOMAIN}-device-update-{self._unique_id}"
async def _async_update_hosts_info(self) -> list[HostAttributes]: async def _async_get_wan_access(self, ip_address: str) -> bool | None:
"""Retrieve latest hosts information from the FRITZ!Box.""" """Get WAN access rule for given IP address."""
try: try:
return await self.hass.async_add_executor_job( wan_access = await self.hass.async_add_executor_job(
self.fritz_hosts.get_hosts_attributes partial(
self.connection.call_action,
"X_AVM-DE_HostFilter:1",
"GetWANAccessByIP",
NewIPv4Address=ip_address,
)
) )
return not wan_access.get("NewDisallow")
except FRITZ_EXCEPTIONS as ex:
_LOGGER.debug(
(
"could not get WAN access rule for client device with IP '%s',"
" error: %s"
),
ip_address,
ex,
)
return None
async def _async_update_hosts_info(self) -> dict[str, Device]:
"""Retrieve latest hosts information from the FRITZ!Box."""
hosts_attributes: list[HostAttributes] = []
hosts_info: list[HostInfo] = []
try:
try:
hosts_attributes = await self.hass.async_add_executor_job(
self.fritz_hosts.get_hosts_attributes
)
except FritzActionError:
hosts_info = await self.hass.async_add_executor_job(
self.fritz_hosts.get_hosts_info
)
except Exception as ex: # pylint: disable=[broad-except] except Exception as ex: # pylint: disable=[broad-except]
if not self.hass.is_stopping: if not self.hass.is_stopping:
raise HomeAssistantError("Error refreshing hosts info") from ex raise HomeAssistantError("Error refreshing hosts info") from ex
return []
hosts: dict[str, Device] = {}
if hosts_attributes:
for attributes in hosts_attributes:
if not attributes.get("MACAddress"):
continue
if (wan_access := attributes.get("X_AVM-DE_WANAccess")) is not None:
wan_access_result = "granted" in wan_access
else:
wan_access_result = None
hosts[attributes["MACAddress"]] = Device(
name=attributes["HostName"],
connected=attributes["Active"],
connected_to="",
connection_type="",
ip_address=attributes["IPAddress"],
ssid=None,
wan_access=wan_access_result,
)
else:
for info in hosts_info:
if not info.get("mac"):
continue
if info["ip"]:
wan_access_result = await self._async_get_wan_access(info["ip"])
else:
wan_access_result = None
hosts[info["mac"]] = Device(
name=info["name"],
connected=info["status"],
connected_to="",
connection_type="",
ip_address=info["ip"],
ssid=None,
wan_access=wan_access_result,
)
return hosts
def _update_device_info(self) -> tuple[bool, str | None, str | None]: def _update_device_info(self) -> tuple[bool, str | None, str | None]:
"""Retrieve latest device information from the FRITZ!Box.""" """Retrieve latest device information from the FRITZ!Box."""
@ -464,25 +543,7 @@ class FritzBoxTools(
consider_home = _default_consider_home consider_home = _default_consider_home
new_device = False new_device = False
hosts = {} hosts = await self._async_update_hosts_info()
for host in await self._async_update_hosts_info():
if not host.get("MACAddress"):
continue
if (wan_access := host.get("X_AVM-DE_WANAccess")) is not None:
wan_access_result = "granted" in wan_access
else:
wan_access_result = None
hosts[host["MACAddress"]] = Device(
name=host["HostName"],
connected=host["Active"],
connected_to="",
connection_type="",
ip_address=host["IPAddress"],
ssid=None,
wan_access=wan_access_result,
)
if not self.fritz_status.device_has_mesh_support or ( if not self.fritz_status.device_has_mesh_support or (
self._options self._options
@ -584,9 +645,7 @@ class FritzBoxTools(
self, config_entry: ConfigEntry | None = None self, config_entry: ConfigEntry | None = None
) -> None: ) -> None:
"""Trigger device trackers cleanup.""" """Trigger device trackers cleanup."""
device_hosts_list = await self.hass.async_add_executor_job( device_hosts = await self._async_update_hosts_info()
self.fritz_hosts.get_hosts_attributes
)
entity_reg: er.EntityRegistry = er.async_get(self.hass) entity_reg: er.EntityRegistry = er.async_get(self.hass)
if config_entry is None: if config_entry is None:
@ -601,9 +660,9 @@ class FritzBoxTools(
device_hosts_macs = set() device_hosts_macs = set()
device_hosts_names = set() device_hosts_names = set()
for device in device_hosts_list: for mac, device in device_hosts.items():
device_hosts_macs.add(device["MACAddress"]) device_hosts_macs.add(mac)
device_hosts_names.add(device["HostName"]) device_hosts_names.add(device.name)
for entry in ha_entity_reg_list: for entry in ha_entity_reg_list:
if entry.original_name is None: if entry.original_name is None:

View File

@ -62,11 +62,11 @@ DESCRIPTIONS = (
GardenaBluetoothNumberEntityDescription( GardenaBluetoothNumberEntityDescription(
key=DeviceConfiguration.rain_pause.uuid, key=DeviceConfiguration.rain_pause.uuid,
translation_key="rain_pause", translation_key="rain_pause",
native_unit_of_measurement=UnitOfTime.DAYS, native_unit_of_measurement=UnitOfTime.MINUTES,
mode=NumberMode.BOX, mode=NumberMode.BOX,
native_min_value=0.0, native_min_value=0.0,
native_max_value=127.0, native_max_value=7 * 24 * 60,
native_step=1.0, native_step=6 * 60.0,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
char=DeviceConfiguration.rain_pause, char=DeviceConfiguration.rain_pause,
), ),

View File

@ -117,3 +117,8 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
self._attr_native_value = time self._attr_native_value = time
super()._handle_coordinator_update() super()._handle_coordinator_update()
return return
@property
def available(self) -> bool:
"""Sensor only available when open."""
return super().available and self._attr_native_value is not None

View File

@ -15,7 +15,8 @@
"cannot_connect": "Failed to connect: {error}" "cannot_connect": "Failed to connect: {error}"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
} }
}, },
"entity": { "entity": {

View File

@ -80,11 +80,11 @@ class TriggerSource:
self._iid_trigger_keys.setdefault(iid, set()).add(trigger_key) self._iid_trigger_keys.setdefault(iid, set()).add(trigger_key)
await connection.add_watchable_characteristics([(aid, iid)]) await connection.add_watchable_characteristics([(aid, iid)])
def fire(self, iid: int, value: dict[str, Any]) -> None: def fire(self, iid: int, ev: dict[str, Any]) -> None:
"""Process events that have been received from a HomeKit accessory.""" """Process events that have been received from a HomeKit accessory."""
for trigger_key in self._iid_trigger_keys.get(iid, set()): for trigger_key in self._iid_trigger_keys.get(iid, set()):
for event_handler in self._callbacks.get(trigger_key, []): for event_handler in self._callbacks.get(trigger_key, []):
event_handler(value) event_handler(ev)
def async_get_triggers(self) -> Generator[tuple[str, str], None, None]: def async_get_triggers(self) -> Generator[tuple[str, str], None, None]:
"""List device triggers for HomeKit devices.""" """List device triggers for HomeKit devices."""
@ -99,20 +99,23 @@ class TriggerSource:
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Attach a trigger.""" """Attach a trigger."""
trigger_data = trigger_info["trigger_data"] trigger_data = trigger_info["trigger_data"]
trigger_key = (config[CONF_TYPE], config[CONF_SUBTYPE]) type_: str = config[CONF_TYPE]
sub_type: str = config[CONF_SUBTYPE]
trigger_key = (type_, sub_type)
job = HassJob(action) job = HassJob(action)
trigger_callbacks = self._callbacks.setdefault(trigger_key, [])
hass = self._hass
@callback @callback
def event_handler(char: dict[str, Any]) -> None: def event_handler(ev: dict[str, Any]) -> None:
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]: if sub_type != HK_TO_HA_INPUT_EVENT_VALUES[ev["value"]]:
return return
self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}}) hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}})
self._callbacks.setdefault(trigger_key, []).append(event_handler) trigger_callbacks.append(event_handler)
def async_remove_handler(): def async_remove_handler():
if trigger_key in self._callbacks: trigger_callbacks.remove(event_handler)
self._callbacks[trigger_key].remove(event_handler)
return async_remove_handler return async_remove_handler
@ -262,7 +265,10 @@ def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], dict[str,
if aid in conn.devices: if aid in conn.devices:
device_id = conn.devices[aid] device_id = conn.devices[aid]
if source := trigger_sources.get(device_id): if source := trigger_sources.get(device_id):
source.fire(iid, ev) # If the value is None, we received the event via polling
# and we don't want to trigger on that
if ev.get("value") is not None:
source.fire(iid, ev)
async def async_get_triggers( async def async_get_triggers(

View File

@ -5,6 +5,7 @@ from typing import Any
from aiohomekit.model import Accessory from aiohomekit.model import Accessory
from aiohomekit.model.characteristics import ( from aiohomekit.model.characteristics import (
EVENT_CHARACTERISTICS,
Characteristic, Characteristic,
CharacteristicPermissions, CharacteristicPermissions,
CharacteristicsTypes, CharacteristicsTypes,
@ -111,7 +112,10 @@ class HomeKitEntity(Entity):
def _setup_characteristic(self, char: Characteristic) -> None: def _setup_characteristic(self, char: Characteristic) -> None:
"""Configure an entity based on a HomeKit characteristics metadata.""" """Configure an entity based on a HomeKit characteristics metadata."""
# Build up a list of (aid, iid) tuples to poll on update() # Build up a list of (aid, iid) tuples to poll on update()
if CharacteristicPermissions.paired_read in char.perms: if (
CharacteristicPermissions.paired_read in char.perms
and char.type not in EVENT_CHARACTERISTICS
):
self.pollable_characteristics.append((self._aid, char.iid)) self.pollable_characteristics.append((self._aid, char.iid))
# Build up a list of (aid, iid) tuples to subscribe to # Build up a list of (aid, iid) tuples to subscribe to

View File

@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"], "loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==2.6.12"], "requirements": ["aiohomekit==2.6.15"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
} }

View File

@ -7,5 +7,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pymazda"], "loggers": ["pymazda"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pymazda==0.3.10"] "requirements": ["pymazda==0.3.11"]
} }

View File

@ -26,7 +26,7 @@
"fields": { "fields": {
"position": { "position": {
"name": "Position", "name": "Position",
"description": "Horizontal vane position. Possible options can be found in the vane_horizontal_positions state attribute.\n." "description": "Horizontal vane position. Possible options can be found in the vane_horizontal_positions state attribute."
} }
} }
}, },
@ -36,7 +36,7 @@
"fields": { "fields": {
"position": { "position": {
"name": "Position", "name": "Position",
"description": "Vertical vane position. Possible options can be found in the vane_vertical_positions state attribute.\n." "description": "Vertical vane position. Possible options can be found in the vane_vertical_positions state attribute."
} }
} }
} }

View File

@ -51,14 +51,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
key="name", key="name",
name="Station name", name="Station name",
device_class=None,
icon="mdi:label-outline", icon="mdi:label-outline",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
SensorEntityDescription( SensorEntityDescription(
key="weather", key="weather",
name="Weather", name="Weather",
device_class=None,
icon="mdi:weather-sunny", # but will adapt to current conditions icon="mdi:weather-sunny", # but will adapt to current conditions
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
), ),
@ -107,7 +105,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
key="visibility", key="visibility",
name="Visibility", name="Visibility",
device_class=None,
icon="mdi:eye", icon="mdi:eye",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
@ -115,14 +112,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
key="visibility_distance", key="visibility_distance",
name="Visibility distance", name="Visibility distance",
native_unit_of_measurement=UnitOfLength.KILOMETERS, native_unit_of_measurement=UnitOfLength.KILOMETERS,
device_class=SensorDeviceClass.DISTANCE,
icon="mdi:eye", icon="mdi:eye",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
SensorEntityDescription( SensorEntityDescription(
key="uv", key="uv",
name="UV index", name="UV index",
device_class=None,
native_unit_of_measurement=UV_INDEX, native_unit_of_measurement=UV_INDEX,
icon="mdi:weather-sunny-alert", icon="mdi:weather-sunny-alert",
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
@ -130,7 +125,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
key="precipitation", key="precipitation",
name="Probability of precipitation", name="Probability of precipitation",
device_class=None,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
icon="mdi:weather-rainy", icon="mdi:weather-rainy",
entity_registry_enabled_default=True, entity_registry_enabled_default=True,

View File

@ -49,7 +49,7 @@ async def async_setup_platform(
hub = get_hub(hass, discovery_info[CONF_NAME]) hub = get_hub(hass, discovery_info[CONF_NAME])
for entry in discovery_info[CONF_SENSORS]: for entry in discovery_info[CONF_SENSORS]:
slave_count = entry.get(CONF_SLAVE_COUNT, 0) slave_count = entry.get(CONF_SLAVE_COUNT, 0)
sensor = ModbusRegisterSensor(hub, entry) sensor = ModbusRegisterSensor(hub, entry, slave_count)
if slave_count > 0: if slave_count > 0:
sensors.extend(await sensor.async_setup_slaves(hass, slave_count, entry)) sensors.extend(await sensor.async_setup_slaves(hass, slave_count, entry))
sensors.append(sensor) sensors.append(sensor)
@ -63,9 +63,12 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
self, self,
hub: ModbusHub, hub: ModbusHub,
entry: dict[str, Any], entry: dict[str, Any],
slave_count: int,
) -> None: ) -> None:
"""Initialize the modbus register sensor.""" """Initialize the modbus register sensor."""
super().__init__(hub, entry) super().__init__(hub, entry)
if slave_count:
self._count = self._count * slave_count
self._coordinator: DataUpdateCoordinator[list[int] | None] | None = None self._coordinator: DataUpdateCoordinator[list[int] | None] | None = None
self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT)
self._attr_state_class = entry.get(CONF_STATE_CLASS) self._attr_state_class = entry.get(CONF_STATE_CLASS)

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nina", "documentation": "https://www.home-assistant.io/integrations/nina",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pynina"], "loggers": ["pynina"],
"requirements": ["PyNINA==0.3.1"] "requirements": ["PyNINA==0.3.2"]
} }

View File

@ -7,5 +7,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["metar", "pynws"], "loggers": ["metar", "pynws"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pynws==1.5.0"] "requirements": ["pynws==1.5.1"]
} }

View File

@ -69,12 +69,12 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
raise ConfigEntryAuthFailed from err raise ConfigEntryAuthFailed from err
forecasts: list[Forecast] = await self.api.async_get_forecast() forecasts: list[Forecast] = await self.api.async_get_forecast()
_LOGGER.debug("Updating sensor data with: %s", forecasts) _LOGGER.debug("Updating sensor data with: %s", forecasts)
await self._insert_statistics([forecast.account for forecast in forecasts]) await self._insert_statistics()
return {forecast.account.utility_account_id: forecast for forecast in forecasts} return {forecast.account.utility_account_id: forecast for forecast in forecasts}
async def _insert_statistics(self, accounts: list[Account]) -> None: async def _insert_statistics(self) -> None:
"""Insert Opower statistics.""" """Insert Opower statistics."""
for account in accounts: for account in await self.api.async_get_accounts():
id_prefix = "_".join( id_prefix = "_".join(
( (
self.api.utility.subdomain(), self.api.utility.subdomain(),

View File

@ -6,5 +6,5 @@
"dependencies": ["recorder"], "dependencies": ["recorder"],
"documentation": "https://www.home-assistant.io/integrations/opower", "documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["opower==0.0.20"] "requirements": ["opower==0.0.26"]
} }

View File

@ -188,7 +188,7 @@ async def async_setup_entry(
sensors = ELEC_SENSORS sensors = ELEC_SENSORS
elif ( elif (
forecast.account.meter_type == MeterType.GAS forecast.account.meter_type == MeterType.GAS
and forecast.unit_of_measure == UnitOfMeasure.THERM and forecast.unit_of_measure in [UnitOfMeasure.THERM, UnitOfMeasure.CCF]
): ):
sensors = GAS_SENSORS sensors = GAS_SENSORS
for sensor in sensors: for sensor in sensors:

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/roborock", "documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["roborock"], "loggers": ["roborock"],
"requirements": ["python-roborock==0.31.1"] "requirements": ["python-roborock==0.32.2"]
} }

View File

@ -15,5 +15,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pysensibo"], "loggers": ["pysensibo"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pysensibo==1.0.32"] "requirements": ["pysensibo==1.0.33"]
} }

View File

@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,
configuration_url=printer.url, configuration_url=printer.url,
connections=device_connections(printer), connections=device_connections(printer),
default_manufacturer="Samsung", manufacturer="Samsung",
identifiers=device_identifiers(printer), identifiers=device_identifiers(printer),
model=printer.model(), model=printer.model(),
name=printer.hostname(), name=printer.hostname(),

View File

@ -163,12 +163,11 @@ class TadoConnector:
def setup(self): def setup(self):
"""Connect to Tado and fetch the zones.""" """Connect to Tado and fetch the zones."""
self.tado = Tado(self._username, self._password) self.tado = Tado(self._username, self._password, None, True)
self.tado.setDebugging(True)
# Load zones and devices # Load zones and devices
self.zones = self.tado.getZones() self.zones = self.tado.get_zones()
self.devices = self.tado.getDevices() self.devices = self.tado.get_devices()
tado_home = self.tado.getMe()["homes"][0] tado_home = self.tado.get_me()["homes"][0]
self.home_id = tado_home["id"] self.home_id = tado_home["id"]
self.home_name = tado_home["name"] self.home_name = tado_home["name"]
@ -181,7 +180,7 @@ class TadoConnector:
def update_devices(self): def update_devices(self):
"""Update the device data from Tado.""" """Update the device data from Tado."""
devices = self.tado.getDevices() devices = self.tado.get_devices()
for device in devices: for device in devices:
device_short_serial_no = device["shortSerialNo"] device_short_serial_no = device["shortSerialNo"]
_LOGGER.debug("Updating device %s", device_short_serial_no) _LOGGER.debug("Updating device %s", device_short_serial_no)
@ -190,7 +189,7 @@ class TadoConnector:
INSIDE_TEMPERATURE_MEASUREMENT INSIDE_TEMPERATURE_MEASUREMENT
in device["characteristics"]["capabilities"] in device["characteristics"]["capabilities"]
): ):
device[TEMP_OFFSET] = self.tado.getDeviceInfo( device[TEMP_OFFSET] = self.tado.get_device_info(
device_short_serial_no, TEMP_OFFSET device_short_serial_no, TEMP_OFFSET
) )
except RuntimeError: except RuntimeError:
@ -218,7 +217,7 @@ class TadoConnector:
def update_zones(self): def update_zones(self):
"""Update the zone data from Tado.""" """Update the zone data from Tado."""
try: try:
zone_states = self.tado.getZoneStates()["zoneStates"] zone_states = self.tado.get_zone_states()["zoneStates"]
except RuntimeError: except RuntimeError:
_LOGGER.error("Unable to connect to Tado while updating zones") _LOGGER.error("Unable to connect to Tado while updating zones")
return return
@ -230,7 +229,7 @@ class TadoConnector:
"""Update the internal data from Tado.""" """Update the internal data from Tado."""
_LOGGER.debug("Updating zone %s", zone_id) _LOGGER.debug("Updating zone %s", zone_id)
try: try:
data = self.tado.getZoneState(zone_id) data = self.tado.get_zone_state(zone_id)
except RuntimeError: except RuntimeError:
_LOGGER.error("Unable to connect to Tado while updating zone %s", zone_id) _LOGGER.error("Unable to connect to Tado while updating zone %s", zone_id)
return return
@ -251,8 +250,8 @@ class TadoConnector:
def update_home(self): def update_home(self):
"""Update the home data from Tado.""" """Update the home data from Tado."""
try: try:
self.data["weather"] = self.tado.getWeather() self.data["weather"] = self.tado.get_weather()
self.data["geofence"] = self.tado.getHomeState() self.data["geofence"] = self.tado.get_home_state()
dispatcher_send( dispatcher_send(
self.hass, self.hass,
SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "home", "data"), SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "home", "data"),
@ -265,15 +264,15 @@ class TadoConnector:
def get_capabilities(self, zone_id): def get_capabilities(self, zone_id):
"""Return the capabilities of the devices.""" """Return the capabilities of the devices."""
return self.tado.getCapabilities(zone_id) return self.tado.get_capabilities(zone_id)
def get_auto_geofencing_supported(self): def get_auto_geofencing_supported(self):
"""Return whether the Tado Home supports auto geofencing.""" """Return whether the Tado Home supports auto geofencing."""
return self.tado.getAutoGeofencingSupported() return self.tado.get_auto_geofencing_supported()
def reset_zone_overlay(self, zone_id): def reset_zone_overlay(self, zone_id):
"""Reset the zone back to the default operation.""" """Reset the zone back to the default operation."""
self.tado.resetZoneOverlay(zone_id) self.tado.reset_zone_overlay(zone_id)
self.update_zone(zone_id) self.update_zone(zone_id)
def set_presence( def set_presence(
@ -282,11 +281,11 @@ class TadoConnector:
): ):
"""Set the presence to home, away or auto.""" """Set the presence to home, away or auto."""
if presence == PRESET_AWAY: if presence == PRESET_AWAY:
self.tado.setAway() self.tado.set_away()
elif presence == PRESET_HOME: elif presence == PRESET_HOME:
self.tado.setHome() self.tado.set_home()
elif presence == PRESET_AUTO: elif presence == PRESET_AUTO:
self.tado.setAuto() self.tado.set_auto()
# Update everything when changing modes # Update everything when changing modes
self.update_zones() self.update_zones()
@ -320,7 +319,7 @@ class TadoConnector:
) )
try: try:
self.tado.setZoneOverlay( self.tado.set_zone_overlay(
zone_id, zone_id,
overlay_mode, overlay_mode,
temperature, temperature,
@ -340,7 +339,7 @@ class TadoConnector:
def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"):
"""Set a zone to off.""" """Set a zone to off."""
try: try:
self.tado.setZoneOverlay( self.tado.set_zone_overlay(
zone_id, overlay_mode, None, None, device_type, "OFF" zone_id, overlay_mode, None, None, device_type, "OFF"
) )
except RequestException as exc: except RequestException as exc:
@ -351,6 +350,6 @@ class TadoConnector:
def set_temperature_offset(self, device_id, offset): def set_temperature_offset(self, device_id, offset):
"""Set temperature offset of device.""" """Set temperature offset of device."""
try: try:
self.tado.setTempOffset(device_id, offset) self.tado.set_temp_offset(device_id, offset)
except RequestException as exc: except RequestException as exc:
_LOGGER.error("Could not set temperature offset: %s", exc) _LOGGER.error("Could not set temperature offset: %s", exc)

View File

@ -14,5 +14,5 @@
}, },
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["PyTado"], "loggers": ["PyTado"],
"requirements": ["python-tado==0.15.0"] "requirements": ["python-tado==0.16.0"]
} }

View File

@ -102,7 +102,7 @@ class TomorrowioOptionsConfigFlow(config_entries.OptionsFlow):
vol.Required( vol.Required(
CONF_TIMESTEP, CONF_TIMESTEP,
default=self._config_entry.options[CONF_TIMESTEP], default=self._config_entry.options[CONF_TIMESTEP],
): vol.In([1, 5, 15, 30]), ): vol.In([1, 5, 15, 30, 60]),
} }
return self.async_show_form( return self.async_show_form(

View File

@ -25,7 +25,7 @@ LOGGER = logging.getLogger(__package__)
CONF_TIMESTEP = "timestep" CONF_TIMESTEP = "timestep"
FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] FORECAST_TYPES = [DAILY, HOURLY, NOWCAST]
DEFAULT_TIMESTEP = 15 DEFAULT_TIMESTEP = 60
DEFAULT_FORECAST_TYPE = DAILY DEFAULT_FORECAST_TYPE = DAILY
DOMAIN = "tomorrowio" DOMAIN = "tomorrowio"
INTEGRATION_NAME = "Tomorrow.io" INTEGRATION_NAME = "Tomorrow.io"

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tplink_omada", "documentation": "https://www.home-assistant.io/integrations/tplink_omada",
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["tplink_omada_client==1.2.4"] "requirements": ["tplink_omada_client==1.3.2"]
} }

View File

@ -43,7 +43,7 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity):
"""Initialize tracker entity.""" """Initialize tracker entity."""
super().__init__(user_id, item.trackable, item.tracker_details) super().__init__(user_id, item.trackable, item.tracker_details)
self._battery_level: int = item.hw_info["battery_level"] self._battery_level: int | None = item.hw_info.get("battery_level")
self._latitude: float = item.pos_report["latlong"][0] self._latitude: float = item.pos_report["latlong"][0]
self._longitude: float = item.pos_report["latlong"][1] self._longitude: float = item.pos_report["latlong"][1]
self._accuracy: int = item.pos_report["pos_uncertainty"] self._accuracy: int = item.pos_report["pos_uncertainty"]
@ -75,7 +75,7 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity):
return self._accuracy return self._accuracy
@property @property
def battery_level(self) -> int: def battery_level(self) -> int | None:
"""Return the battery level of the device.""" """Return the battery level of the device."""
return self._battery_level return self._battery_level

View File

@ -44,6 +44,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = {
"cl": ( "cl": (
TuyaCoverEntityDescription( TuyaCoverEntityDescription(
key=DPCode.CONTROL, key=DPCode.CONTROL,
translation_key="curtain",
current_state=DPCode.SITUATION_SET, current_state=DPCode.SITUATION_SET,
current_position=(DPCode.PERCENT_CONTROL, DPCode.PERCENT_STATE), current_position=(DPCode.PERCENT_CONTROL, DPCode.PERCENT_STATE),
set_position=DPCode.PERCENT_CONTROL, set_position=DPCode.PERCENT_CONTROL,
@ -65,6 +66,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = {
), ),
TuyaCoverEntityDescription( TuyaCoverEntityDescription(
key=DPCode.MACH_OPERATE, key=DPCode.MACH_OPERATE,
translation_key="curtain",
current_position=DPCode.POSITION, current_position=DPCode.POSITION,
set_position=DPCode.POSITION, set_position=DPCode.POSITION,
device_class=CoverDeviceClass.CURTAIN, device_class=CoverDeviceClass.CURTAIN,
@ -76,6 +78,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = {
# It is used by the Kogan Smart Blinds Driver # It is used by the Kogan Smart Blinds Driver
TuyaCoverEntityDescription( TuyaCoverEntityDescription(
key=DPCode.SWITCH_1, key=DPCode.SWITCH_1,
translation_key="blind",
current_position=DPCode.PERCENT_CONTROL, current_position=DPCode.PERCENT_CONTROL,
set_position=DPCode.PERCENT_CONTROL, set_position=DPCode.PERCENT_CONTROL,
device_class=CoverDeviceClass.BLIND, device_class=CoverDeviceClass.BLIND,
@ -111,6 +114,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = {
"clkg": ( "clkg": (
TuyaCoverEntityDescription( TuyaCoverEntityDescription(
key=DPCode.CONTROL, key=DPCode.CONTROL,
translation_key="curtain",
current_position=DPCode.PERCENT_CONTROL, current_position=DPCode.PERCENT_CONTROL,
set_position=DPCode.PERCENT_CONTROL, set_position=DPCode.PERCENT_CONTROL,
device_class=CoverDeviceClass.CURTAIN, device_class=CoverDeviceClass.CURTAIN,
@ -128,6 +132,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = {
"jdcljqr": ( "jdcljqr": (
TuyaCoverEntityDescription( TuyaCoverEntityDescription(
key=DPCode.CONTROL, key=DPCode.CONTROL,
translation_key="curtain",
current_position=DPCode.PERCENT_STATE, current_position=DPCode.PERCENT_STATE,
set_position=DPCode.PERCENT_CONTROL, set_position=DPCode.PERCENT_CONTROL,
device_class=CoverDeviceClass.CURTAIN, device_class=CoverDeviceClass.CURTAIN,

View File

@ -71,6 +71,12 @@
} }
}, },
"cover": { "cover": {
"blind": {
"name": "[%key:component::cover::entity_component::blind::name%]"
},
"curtain": {
"name": "[%key:component::cover::entity_component::curtain::name%]"
},
"curtain_2": { "curtain_2": {
"name": "Curtain 2" "name": "Curtain 2"
}, },

View File

@ -105,11 +105,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = {
translation_key="plug", translation_key="plug",
), ),
), ),
# Cirquit Breaker # Circuit Breaker
"dlq": ( "dlq": (
SwitchEntityDescription( SwitchEntityDescription(
key=DPCode.CHILD_LOCK, key=DPCode.CHILD_LOCK,
translation_key="asd", translation_key="child_lock",
icon="mdi:account-lock", icon="mdi:account-lock",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
), ),

View File

@ -8,7 +8,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiounifi"], "loggers": ["aiounifi"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aiounifi==51"], "requirements": ["aiounifi==52"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@ -79,6 +79,8 @@ def async_wlan_client_value_fn(controller: UniFiController, wlan: Wlan) -> int:
client.mac client.mac
for client in controller.api.clients.values() for client in controller.api.clients.values()
if client.essid == wlan.name if client.essid == wlan.name
and dt_util.utcnow() - dt_util.utc_from_timestamp(client.last_seen or 0)
< controller.option_detection_time
] ]
) )

View File

@ -145,16 +145,26 @@ async def async_handle_webhook(
return Response(status=HTTPStatus.METHOD_NOT_ALLOWED) return Response(status=HTTPStatus.METHOD_NOT_ALLOWED)
if webhook["local_only"] in (True, None) and not isinstance(request, MockRequest): if webhook["local_only"] in (True, None) and not isinstance(request, MockRequest):
if TYPE_CHECKING: if has_cloud := "cloud" in hass.config.components:
assert isinstance(request, Request) from hass_nabucasa import remote # pylint: disable=import-outside-toplevel
assert request.remote is not None
try:
remote = ip_address(request.remote)
except ValueError:
_LOGGER.debug("Unable to parse remote ip %s", request.remote)
return Response(status=HTTPStatus.OK)
if not network.is_local(remote): is_local = True
if has_cloud and remote.is_cloud_request.get():
is_local = False
else:
if TYPE_CHECKING:
assert isinstance(request, Request)
assert request.remote is not None
try:
request_remote = ip_address(request.remote)
except ValueError:
_LOGGER.debug("Unable to parse remote ip %s", request.remote)
return Response(status=HTTPStatus.OK)
is_local = network.is_local(request_remote)
if not is_local:
_LOGGER.warning("Received remote request for local webhook %s", webhook_id) _LOGGER.warning("Received remote request for local webhook %s", webhook_id)
if webhook["local_only"]: if webhook["local_only"]:
return Response(status=HTTPStatus.OK) return Response(status=HTTPStatus.OK)

View File

@ -297,7 +297,7 @@ async def async_setup_entry(
_lights_setup_helper(YeelightColorLightWithNightlightSwitch) _lights_setup_helper(YeelightColorLightWithNightlightSwitch)
_lights_setup_helper(YeelightNightLightModeWithoutBrightnessControl) _lights_setup_helper(YeelightNightLightModeWithoutBrightnessControl)
else: else:
_lights_setup_helper(YeelightColorLightWithoutNightlightSwitch) _lights_setup_helper(YeelightColorLightWithoutNightlightSwitchLight)
elif device_type == BulbType.WhiteTemp: elif device_type == BulbType.WhiteTemp:
if nl_switch_light and device.is_nightlight_supported: if nl_switch_light and device.is_nightlight_supported:
_lights_setup_helper(YeelightWithNightLight) _lights_setup_helper(YeelightWithNightLight)
@ -931,6 +931,14 @@ class YeelightColorLightWithoutNightlightSwitch(
"""Representation of a Color Yeelight light.""" """Representation of a Color Yeelight light."""
class YeelightColorLightWithoutNightlightSwitchLight(
YeelightColorLightWithoutNightlightSwitch
):
"""Representation of a Color Yeelight light."""
_attr_name = None
class YeelightColorLightWithNightlightSwitch( class YeelightColorLightWithNightlightSwitch(
YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight
): ):

View File

@ -17,7 +17,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["async_upnp_client", "yeelight"], "loggers": ["async_upnp_client", "yeelight"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["yeelight==0.7.12", "async-upnp-client==0.34.1"], "requirements": ["yeelight==0.7.13", "async-upnp-client==0.34.1"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_miio._udp.local.", "type": "_miio._udp.local.",

View File

@ -20,7 +20,7 @@
"zigpy_znp" "zigpy_znp"
], ],
"requirements": [ "requirements": [
"bellows==0.35.8", "bellows==0.35.9",
"pyserial==3.5", "pyserial==3.5",
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.102", "zha-quirks==0.0.102",

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023 MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 8 MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "1" PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.8.1" version = "2023.8.2"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"

View File

@ -5,7 +5,7 @@
AEMET-OpenData==0.2.2 AEMET-OpenData==0.2.2
# homeassistant.components.aladdin_connect # homeassistant.components.aladdin_connect
AIOAladdinConnect==0.1.56 AIOAladdinConnect==0.1.57
# homeassistant.components.honeywell # homeassistant.components.honeywell
AIOSomecomfort==0.0.15 AIOSomecomfort==0.0.15
@ -79,7 +79,7 @@ PyMetno==0.10.0
PyMicroBot==0.0.9 PyMicroBot==0.0.9
# homeassistant.components.nina # homeassistant.components.nina
PyNINA==0.3.1 PyNINA==0.3.2
# homeassistant.components.mobile_app # homeassistant.components.mobile_app
# homeassistant.components.owntracks # homeassistant.components.owntracks
@ -191,7 +191,7 @@ aioairq==0.2.4
aioairzone-cloud==0.2.1 aioairzone-cloud==0.2.1
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.6.4 aioairzone==0.6.5
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2023.04.0 aioambient==2023.04.0
@ -249,7 +249,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10 aioharmony==0.2.10
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==2.6.12 aiohomekit==2.6.15
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@ -360,7 +360,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5 aiotractive==0.5.5
# homeassistant.components.unifi # homeassistant.components.unifi
aiounifi==51 aiounifi==52
# homeassistant.components.vlc_telnet # homeassistant.components.vlc_telnet
aiovlc==0.1.0 aiovlc==0.1.0
@ -500,10 +500,10 @@ beautifulsoup4==4.11.1
# beewi-smartclim==0.0.10 # beewi-smartclim==0.0.10
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.35.8 bellows==0.35.9
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer-connected==0.13.8 bimmer-connected==0.13.9
# homeassistant.components.bizkaibus # homeassistant.components.bizkaibus
bizkaibus==0.1.1 bizkaibus==0.1.1
@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16
openwrt-ubus-rpc==0.0.2 openwrt-ubus-rpc==0.0.2
# homeassistant.components.opower # homeassistant.components.opower
opower==0.0.20 opower==0.0.26
# homeassistant.components.oralb # homeassistant.components.oralb
oralb-ble==0.17.6 oralb-ble==0.17.6
@ -1563,7 +1563,7 @@ pyairnow==1.2.1
# homeassistant.components.airvisual # homeassistant.components.airvisual
# homeassistant.components.airvisual_pro # homeassistant.components.airvisual_pro
pyairvisual==2022.12.1 pyairvisual==2023.08.1
# homeassistant.components.atag # homeassistant.components.atag
pyatag==0.3.5.3 pyatag==0.3.5.3
@ -1572,7 +1572,7 @@ pyatag==0.3.5.3
pyatmo==7.5.0 pyatmo==7.5.0
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.13.3 pyatv==0.13.4
# homeassistant.components.aussie_broadband # homeassistant.components.aussie_broadband
pyaussiebb==0.0.15 pyaussiebb==0.0.15
@ -1824,7 +1824,7 @@ pymailgunner==1.4
pymata-express==1.19 pymata-express==1.19
# homeassistant.components.mazda # homeassistant.components.mazda
pymazda==0.3.10 pymazda==0.3.11
# homeassistant.components.mediaroom # homeassistant.components.mediaroom
pymediaroom==0.6.5.4 pymediaroom==0.6.5.4
@ -1872,7 +1872,7 @@ pynuki==1.6.2
pynut2==2.1.2 pynut2==2.1.2
# homeassistant.components.nws # homeassistant.components.nws
pynws==1.5.0 pynws==1.5.1
# homeassistant.components.nx584 # homeassistant.components.nx584
pynx584==0.5 pynx584==0.5
@ -1982,7 +1982,7 @@ pysabnzbd==1.1.1
pysaj==0.0.16 pysaj==0.0.16
# homeassistant.components.sensibo # homeassistant.components.sensibo
pysensibo==1.0.32 pysensibo==1.0.33
# homeassistant.components.serial # homeassistant.components.serial
# homeassistant.components.zha # homeassistant.components.zha
@ -2150,7 +2150,7 @@ python-qbittorrent==0.4.3
python-ripple-api==0.0.3 python-ripple-api==0.0.3
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==0.31.1 python-roborock==0.32.2
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.33 python-smarttub==0.0.33
@ -2159,7 +2159,7 @@ python-smarttub==0.0.33
python-songpal==0.15.2 python-songpal==0.15.2
# homeassistant.components.tado # homeassistant.components.tado
python-tado==0.15.0 python-tado==0.16.0
# homeassistant.components.telegram_bot # homeassistant.components.telegram_bot
python-telegram-bot==13.1 python-telegram-bot==13.1
@ -2563,7 +2563,7 @@ total-connect-client==2023.2
tp-connected==0.0.4 tp-connected==0.0.4
# homeassistant.components.tplink_omada # homeassistant.components.tplink_omada
tplink_omada_client==1.2.4 tplink_omada_client==1.3.2
# homeassistant.components.transmission # homeassistant.components.transmission
transmission-rpc==4.1.5 transmission-rpc==4.1.5
@ -2725,7 +2725,7 @@ yalexs-ble==2.2.3
yalexs==1.5.1 yalexs==1.5.1
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.12 yeelight==0.7.13
# homeassistant.components.yeelightsunflower # homeassistant.components.yeelightsunflower
yeelightsunflower==0.0.10 yeelightsunflower==0.0.10

View File

@ -7,7 +7,7 @@
AEMET-OpenData==0.2.2 AEMET-OpenData==0.2.2
# homeassistant.components.aladdin_connect # homeassistant.components.aladdin_connect
AIOAladdinConnect==0.1.56 AIOAladdinConnect==0.1.57
# homeassistant.components.honeywell # homeassistant.components.honeywell
AIOSomecomfort==0.0.15 AIOSomecomfort==0.0.15
@ -69,7 +69,7 @@ PyMetno==0.10.0
PyMicroBot==0.0.9 PyMicroBot==0.0.9
# homeassistant.components.nina # homeassistant.components.nina
PyNINA==0.3.1 PyNINA==0.3.2
# homeassistant.components.mobile_app # homeassistant.components.mobile_app
# homeassistant.components.owntracks # homeassistant.components.owntracks
@ -172,7 +172,7 @@ aioairq==0.2.4
aioairzone-cloud==0.2.1 aioairzone-cloud==0.2.1
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.6.4 aioairzone==0.6.5
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2023.04.0 aioambient==2023.04.0
@ -227,7 +227,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10 aioharmony==0.2.10
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==2.6.12 aiohomekit==2.6.15
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@ -335,7 +335,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5 aiotractive==0.5.5
# homeassistant.components.unifi # homeassistant.components.unifi
aiounifi==51 aiounifi==52
# homeassistant.components.vlc_telnet # homeassistant.components.vlc_telnet
aiovlc==0.1.0 aiovlc==0.1.0
@ -424,10 +424,10 @@ base36==0.1.1
beautifulsoup4==4.11.1 beautifulsoup4==4.11.1
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.35.8 bellows==0.35.9
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer-connected==0.13.8 bimmer-connected==0.13.9
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bleak-retry-connector==3.1.1 bleak-retry-connector==3.1.1
@ -1037,7 +1037,7 @@ openerz-api==0.2.0
openhomedevice==2.2.0 openhomedevice==2.2.0
# homeassistant.components.opower # homeassistant.components.opower
opower==0.0.20 opower==0.0.26
# homeassistant.components.oralb # homeassistant.components.oralb
oralb-ble==0.17.6 oralb-ble==0.17.6
@ -1169,7 +1169,7 @@ pyairnow==1.2.1
# homeassistant.components.airvisual # homeassistant.components.airvisual
# homeassistant.components.airvisual_pro # homeassistant.components.airvisual_pro
pyairvisual==2022.12.1 pyairvisual==2023.08.1
# homeassistant.components.atag # homeassistant.components.atag
pyatag==0.3.5.3 pyatag==0.3.5.3
@ -1178,7 +1178,7 @@ pyatag==0.3.5.3
pyatmo==7.5.0 pyatmo==7.5.0
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.13.3 pyatv==0.13.4
# homeassistant.components.aussie_broadband # homeassistant.components.aussie_broadband
pyaussiebb==0.0.15 pyaussiebb==0.0.15
@ -1352,7 +1352,7 @@ pymailgunner==1.4
pymata-express==1.19 pymata-express==1.19
# homeassistant.components.mazda # homeassistant.components.mazda
pymazda==0.3.10 pymazda==0.3.11
# homeassistant.components.melcloud # homeassistant.components.melcloud
pymelcloud==2.5.8 pymelcloud==2.5.8
@ -1388,7 +1388,7 @@ pynuki==1.6.2
pynut2==2.1.2 pynut2==2.1.2
# homeassistant.components.nws # homeassistant.components.nws
pynws==1.5.0 pynws==1.5.1
# homeassistant.components.nx584 # homeassistant.components.nx584
pynx584==0.5 pynx584==0.5
@ -1474,7 +1474,7 @@ pyrympro==0.0.7
pysabnzbd==1.1.1 pysabnzbd==1.1.1
# homeassistant.components.sensibo # homeassistant.components.sensibo
pysensibo==1.0.32 pysensibo==1.0.33
# homeassistant.components.serial # homeassistant.components.serial
# homeassistant.components.zha # homeassistant.components.zha
@ -1579,7 +1579,7 @@ python-picnic-api==1.1.0
python-qbittorrent==0.4.3 python-qbittorrent==0.4.3
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==0.31.1 python-roborock==0.32.2
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.33 python-smarttub==0.0.33
@ -1588,7 +1588,7 @@ python-smarttub==0.0.33
python-songpal==0.15.2 python-songpal==0.15.2
# homeassistant.components.tado # homeassistant.components.tado
python-tado==0.15.0 python-tado==0.16.0
# homeassistant.components.telegram_bot # homeassistant.components.telegram_bot
python-telegram-bot==13.1 python-telegram-bot==13.1
@ -1869,7 +1869,7 @@ toonapi==0.2.1
total-connect-client==2023.2 total-connect-client==2023.2
# homeassistant.components.tplink_omada # homeassistant.components.tplink_omada
tplink_omada_client==1.2.4 tplink_omada_client==1.3.2
# homeassistant.components.transmission # homeassistant.components.transmission
transmission-rpc==4.1.5 transmission-rpc==4.1.5
@ -2007,7 +2007,7 @@ yalexs-ble==2.2.3
yalexs==1.5.1 yalexs==1.5.1
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.12 yeelight==0.7.13
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.3.0 yolink-api==0.3.0

View File

@ -329,7 +329,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None:
await async_init_integration(hass) await async_init_integration(hass)
HVAC_MOCK = { HVAC_MOCK_1 = {
API_DATA: [ API_DATA: [
{ {
API_SYSTEM_ID: 1, API_SYSTEM_ID: 1,
@ -340,7 +340,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None:
} }
with patch( with patch(
"homeassistant.components.airzone.AirzoneLocalApi.put_hvac", "homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
return_value=HVAC_MOCK, return_value=HVAC_MOCK_1,
): ):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
@ -407,6 +407,51 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None:
state = hass.states.get("climate.airzone_2_1") state = hass.states.get("climate.airzone_2_1")
assert state.state == HVACMode.HEAT_COOL assert state.state == HVACMode.HEAT_COOL
HVAC_MOCK_4 = {
API_DATA: [
{
API_SYSTEM_ID: 1,
API_ZONE_ID: 1,
API_ON: 1,
}
]
}
with patch(
"homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
return_value=HVAC_MOCK_4,
):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{
ATTR_ENTITY_ID: "climate.salon",
ATTR_HVAC_MODE: HVACMode.FAN_ONLY,
},
blocking=True,
)
state = hass.states.get("climate.salon")
assert state.state == HVACMode.FAN_ONLY
HVAC_MOCK_NO_SET_POINT = {**HVAC_MOCK}
del HVAC_MOCK_NO_SET_POINT[API_SYSTEMS][0][API_DATA][0][API_SET_POINT]
with patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac",
return_value=HVAC_MOCK_NO_SET_POINT,
), patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems",
return_value=HVAC_SYSTEMS_MOCK,
), patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_webserver",
return_value=HVAC_WEBSERVER_MOCK,
):
async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
state = hass.states.get("climate.salon")
assert state.attributes.get(ATTR_TEMPERATURE) == 19.1
async def test_airzone_climate_set_hvac_slave_error(hass: HomeAssistant) -> None: async def test_airzone_climate_set_hvac_slave_error(hass: HomeAssistant) -> None:
"""Test setting the HVAC mode for a slave zone.""" """Test setting the HVAC mode for a slave zone."""

View File

@ -36,6 +36,7 @@ from tests.common import async_mock_service
NAME = "alert_test" NAME = "alert_test"
DONE_MESSAGE = "alert_gone" DONE_MESSAGE = "alert_gone"
NOTIFIER = "test" NOTIFIER = "test"
BAD_NOTIFIER = "bad_notifier"
TEMPLATE = "{{ states.sensor.test.entity_id }}" TEMPLATE = "{{ states.sensor.test.entity_id }}"
TEST_ENTITY = "sensor.test" TEST_ENTITY = "sensor.test"
TITLE = "{{ states.sensor.test.entity_id }}" TITLE = "{{ states.sensor.test.entity_id }}"
@ -199,6 +200,26 @@ async def test_notification(
assert len(mock_notifier) == 2 assert len(mock_notifier) == 2
async def test_bad_notifier(
hass: HomeAssistant, mock_notifier: list[ServiceCall]
) -> None:
"""Test a broken notifier does not break the alert."""
config = deepcopy(TEST_CONFIG)
config[DOMAIN][NAME][CONF_NOTIFIERS] = [BAD_NOTIFIER, NOTIFIER]
assert await async_setup_component(hass, DOMAIN, config)
assert len(mock_notifier) == 0
hass.states.async_set("sensor.test", STATE_ON)
await hass.async_block_till_done()
assert len(mock_notifier) == 1
assert hass.states.get(ENTITY_ID).state == STATE_ON
hass.states.async_set("sensor.test", STATE_OFF)
await hass.async_block_till_done()
assert len(mock_notifier) == 2
assert hass.states.get(ENTITY_ID).state == STATE_IDLE
async def test_no_notifiers( async def test_no_notifiers(
hass: HomeAssistant, mock_notifier: list[ServiceCall] hass: HomeAssistant, mock_notifier: list[ServiceCall]
) -> None: ) -> None:

View File

@ -90,6 +90,7 @@ async def test_user_flow_cannot_connect(
host = "1.2.3.4" host = "1.2.3.4"
mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True)
mock_api.async_get_name_and_mac = AsyncMock(side_effect=CannotConnect()) mock_api.async_get_name_and_mac = AsyncMock(side_effect=CannotConnect())
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@ -101,6 +102,7 @@ async def test_user_flow_cannot_connect(
assert "host" in result["data_schema"].schema assert "host" in result["data_schema"].schema
assert result["errors"] == {"base": "cannot_connect"} assert result["errors"] == {"base": "cannot_connect"}
mock_api.async_generate_cert_if_missing.assert_called()
mock_api.async_get_name_and_mac.assert_called() mock_api.async_get_name_and_mac.assert_called()
mock_api.async_start_pairing.assert_not_called() mock_api.async_start_pairing.assert_not_called()
@ -329,6 +331,7 @@ async def test_user_flow_already_configured_host_changed_reloads_entry(
assert "host" in result["data_schema"].schema assert "host" in result["data_schema"].schema
assert not result["errors"] assert not result["errors"]
mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True)
mock_api.async_get_name_and_mac = AsyncMock(return_value=(name, mac)) mock_api.async_get_name_and_mac = AsyncMock(return_value=(name, mac))
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@ -338,6 +341,7 @@ async def test_user_flow_already_configured_host_changed_reloads_entry(
assert result.get("type") == FlowResultType.ABORT assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "already_configured" assert result.get("reason") == "already_configured"
mock_api.async_generate_cert_if_missing.assert_called()
mock_api.async_get_name_and_mac.assert_called() mock_api.async_get_name_and_mac.assert_called()
mock_api.async_start_pairing.assert_not_called() mock_api.async_start_pairing.assert_not_called()
@ -386,6 +390,7 @@ async def test_user_flow_already_configured_host_not_changed_no_reload_entry(
assert "host" in result["data_schema"].schema assert "host" in result["data_schema"].schema
assert not result["errors"] assert not result["errors"]
mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True)
mock_api.async_get_name_and_mac = AsyncMock(return_value=(name, mac)) mock_api.async_get_name_and_mac = AsyncMock(return_value=(name, mac))
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@ -395,6 +400,7 @@ async def test_user_flow_already_configured_host_not_changed_no_reload_entry(
assert result.get("type") == FlowResultType.ABORT assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "already_configured" assert result.get("reason") == "already_configured"
mock_api.async_generate_cert_if_missing.assert_called()
mock_api.async_get_name_and_mac.assert_called() mock_api.async_get_name_and_mac.assert_called()
mock_api.async_start_pairing.assert_not_called() mock_api.async_start_pairing.assert_not_called()

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import subprocess
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
@ -16,7 +17,7 @@ from homeassistant.components.homeassistant import (
SERVICE_UPDATE_ENTITY, SERVICE_UPDATE_ENTITY,
) )
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir import homeassistant.helpers.issue_registry as ir
@ -697,3 +698,40 @@ async def test_scrape_sensor_device_date(
entity_state = hass.states.get("sensor.test") entity_state = hass.states.get("sensor.test")
assert entity_state assert entity_state
assert entity_state.state == "2022-01-17" assert entity_state.state == "2022-01-17"
async def test_template_not_error_when_data_is_none(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test command sensor with template not logging error when data is None."""
with patch(
"homeassistant.components.command_line.utils.subprocess.check_output",
side_effect=subprocess.CalledProcessError,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"sensor": {
"name": "Test",
"command": "failed command",
"unit_of_measurement": "MB",
"value_template": "{{ (value.split('\t')[0]|int(0)/1000)|round(3) }}",
}
}
]
},
)
await hass.async_block_till_done()
entity_state = hass.states.get("sensor.test")
assert entity_state
assert entity_state.state == STATE_UNKNOWN
assert (
"Template variable error: 'None' has no attribute 'split' when rendering"
not in caplog.text
)

View File

@ -35,7 +35,7 @@
'entity_id': 'sensor.mock_title_valve_closing', 'entity_id': 'sensor.mock_title_valve_closing',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unknown', 'state': 'unavailable',
}) })
# --- # ---
# name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery] # name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery]

View File

@ -425,6 +425,14 @@ async def test_handle_events_late_setup(hass: HomeAssistant, utcnow, calls) -> N
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data["some"] == "device - button1 - single_press - 0" assert calls[0].data["some"] == "device - button1 - single_press - 0"
# Make sure automation doesn't trigger for a polled None
helper.pairing.testing.update_named_service(
"Button 1", {CharacteristicsTypes.INPUT_EVENT: None}
)
await hass.async_block_till_done()
assert len(calls) == 1
# Make sure automation doesn't trigger for long press # Make sure automation doesn't trigger for long press
helper.pairing.testing.update_named_service( helper.pairing.testing.update_named_service(
"Button 1", {CharacteristicsTypes.INPUT_EVENT: 1} "Button 1", {CharacteristicsTypes.INPUT_EVENT: 1}

View File

@ -67,6 +67,10 @@ async def setup_entry(
return_value=PROP, return_value=PROP,
), patch( ), patch(
"homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message"
), patch(
"homeassistant.components.roborock.RoborockMqttClient._wait_response"
), patch(
"homeassistant.components.roborock.coordinator.RoborockLocalClient._wait_response"
): ):
assert await async_setup_component(hass, DOMAIN, {}) assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -547,6 +547,163 @@
"autoOffEnabled": false, "autoOffEnabled": false,
"antiMoldTimer": null, "antiMoldTimer": null,
"antiMoldConfig": null "antiMoldConfig": null
},
{
"isGeofenceOnEnterEnabledForThisUser": false,
"isClimateReactGeofenceOnEnterEnabledForThisUser": false,
"isMotionGeofenceOnEnterEnabled": false,
"isOwner": true,
"id": "BBZZBBZZ",
"qrId": "AAAAAAAACC",
"temperatureUnit": "C",
"room": {
"uid": "99YY99YY",
"name": "Bedroom",
"icon": "Diningroom"
},
"acState": {
"timestamp": {
"time": "2022-04-30T11:23:30.067312Z",
"secondsAgo": -1
},
"on": false
},
"lastStateChange": {
"time": "2022-04-30T11:21:41Z",
"secondsAgo": 108
},
"lastStateChangeToOn": {
"time": "2022-04-30T09:43:26Z",
"secondsAgo": 6003
},
"lastStateChangeToOff": {
"time": "2022-04-30T11:21:37Z",
"secondsAgo": 112
},
"location": {
"id": "ZZZZZZZZZZYY",
"name": "Home",
"latLon": [58.9806976, 20.5864297],
"address": ["Sealand 99", "Some county"],
"country": "United Country",
"createTime": {
"time": "2020-03-21T15:44:15Z",
"secondsAgo": 66543240
}
},
"connectionStatus": {
"isAlive": true,
"lastSeen": {
"time": "2022-04-30T11:23:20.642798Z",
"secondsAgo": 9
}
},
"firmwareVersion": "PUR00111",
"firmwareType": "pure-esp32",
"productModel": "pure",
"configGroup": "stable",
"currentlyAvailableFirmwareVersion": "PUR00111",
"cleanFiltersNotificationEnabled": false,
"shouldShowFilterCleaningNotification": false,
"isGeofenceOnExitEnabled": false,
"isClimateReactGeofenceOnExitEnabled": false,
"isMotionGeofenceOnExitEnabled": false,
"serial": "0987654321",
"sensorsCalibration": {
"temperature": 0.0,
"humidity": 0.0
},
"motionSensors": [],
"tags": [],
"timer": null,
"schedules": [],
"motionConfig": null,
"filtersCleaning": {
"acOnSecondsSinceLastFiltersClean": 415560,
"filtersCleanSecondsThreshold": 14256000,
"lastFiltersCleanTime": {
"time": "2022-04-23T15:58:45Z",
"secondsAgo": 588284
},
"shouldCleanFilters": false
},
"serviceSubscriptions": [],
"roomIsOccupied": null,
"mainMeasurementsSensor": null,
"pureBoostConfig": null,
"warrantyEligible": "no",
"warrantyEligibleUntil": {
"time": "2022-04-10T09:58:58Z",
"secondsAgo": 1733071
},
"features": ["optimusTrial", "softShowPlus"],
"runningHealthcheck": null,
"lastHealthcheck": null,
"lastACStateChange": {
"id": "AA22",
"time": {
"time": "2022-04-30T11:21:37Z",
"secondsAgo": 112
},
"status": "Success",
"acState": {
"timestamp": {
"time": "2022-04-30T11:23:30.090144Z",
"secondsAgo": -1
},
"on": false,
"mode": "fan",
"fanLevel": "low",
"light": "on"
},
"resultingAcState": {
"timestamp": {
"time": "2022-04-30T11:23:30.090185Z",
"secondsAgo": -1
},
"on": false,
"mode": "fan",
"fanLevel": "low",
"light": "on"
},
"changedProperties": ["on"],
"reason": "UserRequest",
"failureReason": null,
"resolveTime": {
"time": "2022-04-30T11:21:37Z",
"secondsAgo": 112
},
"causedByScheduleId": null,
"causedByScheduleType": null
},
"homekitSupported": true,
"remoteCapabilities": null,
"remote": {
"toggle": false,
"window": false
},
"remoteFlavor": "Eccentric Eagle",
"remoteAlternatives": [],
"smartMode": null,
"measurements": {
"time": {
"time": "2022-04-30T11:23:20.642798Z",
"secondsAgo": 9
},
"rssi": -58,
"pm25": 1,
"motion": false,
"roomIsOccupied": null
},
"accessPoint": {
"ssid": "Sensibo-09876",
"password": null
},
"macAddress": "00:01:00:01:00:01",
"autoOffMinutes": null,
"autoOffEnabled": false,
"antiMoldTimer": null,
"antiMoldConfig": null
} }
] ]
} }

View File

@ -76,12 +76,16 @@ async def test_climate_find_valid_targets() -> None:
async def test_climate( async def test_climate(
hass: HomeAssistant, load_int: ConfigEntry, get_data: SensiboData hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
get_data: SensiboData,
load_int: ConfigEntry,
) -> None: ) -> None:
"""Test the Sensibo climate.""" """Test the Sensibo climate."""
state1 = hass.states.get("climate.hallway") state1 = hass.states.get("climate.hallway")
state2 = hass.states.get("climate.kitchen") state2 = hass.states.get("climate.kitchen")
state3 = hass.states.get("climate.bedroom")
assert state1.state == "heat" assert state1.state == "heat"
assert state1.attributes == { assert state1.attributes == {
@ -113,6 +117,19 @@ async def test_climate(
assert state2.state == "off" assert state2.state == "off"
assert not state3
found_log = False
logs = caplog.get_records("setup")
for log in logs:
if (
log.message
== "Device Bedroom not correctly registered with Sensibo cloud. Skipping device"
):
found_log = True
break
assert found_log
async def test_climate_fan( async def test_climate_fan(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -470,6 +470,7 @@ async def test_wlan_client_sensors(
wireless_client_1 = { wireless_client_1 = {
"essid": "SSID 1", "essid": "SSID 1",
"is_wired": False, "is_wired": False,
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
"mac": "00:00:00:00:00:01", "mac": "00:00:00:00:00:01",
"name": "Wireless client", "name": "Wireless client",
"oui": "Producer", "oui": "Producer",
@ -479,6 +480,7 @@ async def test_wlan_client_sensors(
wireless_client_2 = { wireless_client_2 = {
"essid": "SSID 2", "essid": "SSID 2",
"is_wired": False, "is_wired": False,
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
"mac": "00:00:00:00:00:02", "mac": "00:00:00:00:00:02",
"name": "Wireless client2", "name": "Wireless client2",
"oui": "Producer2", "oui": "Producer2",
@ -526,9 +528,17 @@ async def test_wlan_client_sensors(
# Verify state update - decreasing number # Verify state update - decreasing number
wireless_client_1["essid"] = "SSID" wireless_client_1["essid"] = "SSID"
wireless_client_2["essid"] = "SSID"
mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_1) mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_1)
async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL)
await hass.async_block_till_done()
ssid_1 = hass.states.get("sensor.ssid_1")
assert ssid_1.state == "1"
# Verify state update - decreasing number
wireless_client_2["last_seen"] = 0
mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_2) mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_2)
async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL)

View File

@ -1,7 +1,7 @@
"""Test the webhook component.""" """Test the webhook component."""
from http import HTTPStatus from http import HTTPStatus
from ipaddress import ip_address from ipaddress import ip_address
from unittest.mock import patch from unittest.mock import Mock, patch
from aiohttp import web from aiohttp import web
import pytest import pytest
@ -206,6 +206,8 @@ async def test_webhook_not_allowed_method(hass: HomeAssistant) -> None:
async def test_webhook_local_only(hass: HomeAssistant, mock_client) -> None: async def test_webhook_local_only(hass: HomeAssistant, mock_client) -> None:
"""Test posting a webhook with local only.""" """Test posting a webhook with local only."""
hass.config.components.add("cloud")
hooks = [] hooks = []
webhook_id = webhook.async_generate_id() webhook_id = webhook.async_generate_id()
@ -234,6 +236,16 @@ async def test_webhook_local_only(hass: HomeAssistant, mock_client) -> None:
# No hook received # No hook received
assert len(hooks) == 1 assert len(hooks) == 1
# Request from Home Assistant Cloud remote UI
with patch(
"hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True))
):
resp = await mock_client.post(f"/api/webhook/{webhook_id}", json={"data": True})
# No hook received
assert resp.status == HTTPStatus.OK
assert len(hooks) == 1
async def test_listing_webhook( async def test_listing_webhook(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -1,6 +1,6 @@
"""The tests for the webhook automation trigger.""" """The tests for the webhook automation trigger."""
from ipaddress import ip_address from ipaddress import ip_address
from unittest.mock import patch from unittest.mock import Mock, patch
import pytest import pytest
@ -68,6 +68,9 @@ async def test_webhook_post(
hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator
) -> None: ) -> None:
"""Test triggering with a POST webhook.""" """Test triggering with a POST webhook."""
# Set up fake cloud
hass.config.components.add("cloud")
events = [] events = []
@callback @callback
@ -114,6 +117,16 @@ async def test_webhook_post(
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(events) == 1 assert len(events) == 1
# Request from Home Assistant Cloud remote UI
with patch(
"hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True))
):
await client.post("/api/webhook/post_webhook", data={"hello": "world"})
# No hook received
await hass.async_block_till_done()
assert len(events) == 1
async def test_webhook_allowed_methods_internet( async def test_webhook_allowed_methods_internet(
hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator
@ -141,7 +154,6 @@ async def test_webhook_allowed_methods_internet(
}, },
"action": { "action": {
"event": "test_success", "event": "test_success",
"event_data_template": {"hello": "yo {{ trigger.data.hello }}"},
}, },
} }
}, },
@ -150,7 +162,7 @@ async def test_webhook_allowed_methods_internet(
client = await hass_client_no_auth() client = await hass_client_no_auth()
await client.post("/api/webhook/post_webhook", data={"hello": "world"}) await client.post("/api/webhook/post_webhook")
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(events) == 0 assert len(events) == 0
@ -160,7 +172,7 @@ async def test_webhook_allowed_methods_internet(
"homeassistant.components.webhook.ip_address", "homeassistant.components.webhook.ip_address",
return_value=ip_address("123.123.123.123"), return_value=ip_address("123.123.123.123"),
): ):
await client.put("/api/webhook/post_webhook", data={"hello": "world"}) await client.put("/api/webhook/post_webhook")
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(events) == 1 assert len(events) == 1