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",
"iot_class": "cloud_polling",
"loggers": ["pyairvisual", "pysmb"],
"requirements": ["pyairvisual==2022.12.1"]
"requirements": ["pyairvisual==2023.08.1"]
}

View File

@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"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",
"iot_class": "local_polling",
"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",
"iot_class": "cloud_polling",
"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,
)
from homeassistant.core import HassJob, HomeAssistant
from homeassistant.exceptions import ServiceNotFound
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@ -293,9 +294,15 @@ class Alert(Entity):
LOGGER.debug(msg_payload)
for target in self._notifiers:
await self.hass.services.async_call(
DOMAIN_NOTIFY, target, msg_payload, context=self._context
)
try:
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 Unacknowledge alert."""

View File

@ -58,6 +58,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
assert self.host
api = create_api(self.hass, self.host, enable_ime=False)
try:
await api.async_generate_cert_if_missing()
self.name, self.mac = await api.async_get_name_and_mac()
assert 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",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.13.3"],
"requirements": ["pyatv==0.13.4"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling",
"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."""
_attr_has_entity_name = True
_attr_name = None
def __init__(self, device):
"""Initialize the light."""

View File

@ -207,7 +207,8 @@ class CommandSensor(ManualTriggerEntity, SensorEntity):
self._process_manual_data(value)
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,
None,
@ -221,7 +222,6 @@ class CommandSensor(ManualTriggerEntity, SensorEntity):
self._process_manual_data(value)
return
self._attr_native_value = None
if value is not None:
self._attr_native_value = async_parse_date_datetime(
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):
"""Update coordinator data type."""
@ -380,16 +389,86 @@ class FritzBoxTools(
"""Event specific per FRITZ!Box entry to signal updates in devices."""
return f"{DOMAIN}-device-update-{self._unique_id}"
async def _async_update_hosts_info(self) -> list[HostAttributes]:
"""Retrieve latest hosts information from the FRITZ!Box."""
async def _async_get_wan_access(self, ip_address: str) -> bool | None:
"""Get WAN access rule for given IP address."""
try:
return await self.hass.async_add_executor_job(
self.fritz_hosts.get_hosts_attributes
wan_access = await self.hass.async_add_executor_job(
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]
if not self.hass.is_stopping:
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]:
"""Retrieve latest device information from the FRITZ!Box."""
@ -464,25 +543,7 @@ class FritzBoxTools(
consider_home = _default_consider_home
new_device = False
hosts = {}
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,
)
hosts = await self._async_update_hosts_info()
if not self.fritz_status.device_has_mesh_support or (
self._options
@ -584,9 +645,7 @@ class FritzBoxTools(
self, config_entry: ConfigEntry | None = None
) -> None:
"""Trigger device trackers cleanup."""
device_hosts_list = await self.hass.async_add_executor_job(
self.fritz_hosts.get_hosts_attributes
)
device_hosts = await self._async_update_hosts_info()
entity_reg: er.EntityRegistry = er.async_get(self.hass)
if config_entry is None:
@ -601,9 +660,9 @@ class FritzBoxTools(
device_hosts_macs = set()
device_hosts_names = set()
for device in device_hosts_list:
device_hosts_macs.add(device["MACAddress"])
device_hosts_names.add(device["HostName"])
for mac, device in device_hosts.items():
device_hosts_macs.add(mac)
device_hosts_names.add(device.name)
for entry in ha_entity_reg_list:
if entry.original_name is None:

View File

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

View File

@ -117,3 +117,8 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
self._attr_native_value = time
super()._handle_coordinator_update()
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}"
},
"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": {

View File

@ -80,11 +80,11 @@ class TriggerSource:
self._iid_trigger_keys.setdefault(iid, set()).add(trigger_key)
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."""
for trigger_key in self._iid_trigger_keys.get(iid, set()):
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]:
"""List device triggers for HomeKit devices."""
@ -99,20 +99,23 @@ class TriggerSource:
) -> CALLBACK_TYPE:
"""Attach a trigger."""
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)
trigger_callbacks = self._callbacks.setdefault(trigger_key, [])
hass = self._hass
@callback
def event_handler(char: dict[str, Any]) -> None:
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
def event_handler(ev: dict[str, Any]) -> None:
if sub_type != HK_TO_HA_INPUT_EVENT_VALUES[ev["value"]]:
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():
if trigger_key in self._callbacks:
self._callbacks[trigger_key].remove(event_handler)
trigger_callbacks.remove(event_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:
device_id = conn.devices[aid]
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(

View File

@ -5,6 +5,7 @@ from typing import Any
from aiohomekit.model import Accessory
from aiohomekit.model.characteristics import (
EVENT_CHARACTERISTICS,
Characteristic,
CharacteristicPermissions,
CharacteristicsTypes,
@ -111,7 +112,10 @@ class HomeKitEntity(Entity):
def _setup_characteristic(self, char: Characteristic) -> None:
"""Configure an entity based on a HomeKit characteristics metadata."""
# 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))
# 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",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==2.6.12"],
"requirements": ["aiohomekit==2.6.15"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}

View File

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

View File

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

View File

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

View File

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

View File

@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["metar", "pynws"],
"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
forecasts: list[Forecast] = await self.api.async_get_forecast()
_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}
async def _insert_statistics(self, accounts: list[Account]) -> None:
async def _insert_statistics(self) -> None:
"""Insert Opower statistics."""
for account in accounts:
for account in await self.api.async_get_accounts():
id_prefix = "_".join(
(
self.api.utility.subdomain(),

View File

@ -6,5 +6,5 @@
"dependencies": ["recorder"],
"documentation": "https://www.home-assistant.io/integrations/opower",
"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
elif (
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
for sensor in sensors:

View File

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

View File

@ -15,5 +15,5 @@
"iot_class": "cloud_polling",
"loggers": ["pysensibo"],
"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,
configuration_url=printer.url,
connections=device_connections(printer),
default_manufacturer="Samsung",
manufacturer="Samsung",
identifiers=device_identifiers(printer),
model=printer.model(),
name=printer.hostname(),

View File

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

View File

@ -14,5 +14,5 @@
},
"iot_class": "cloud_polling",
"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(
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(

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tplink_omada",
"integration_type": "hub",
"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."""
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._longitude: float = item.pos_report["latlong"][1]
self._accuracy: int = item.pos_report["pos_uncertainty"]
@ -75,7 +75,7 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity):
return self._accuracy
@property
def battery_level(self) -> int:
def battery_level(self) -> int | None:
"""Return the battery level of the device."""
return self._battery_level

View File

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

View File

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

View File

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

View File

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

View File

@ -79,6 +79,8 @@ def async_wlan_client_value_fn(controller: UniFiController, wlan: Wlan) -> int:
client.mac
for client in controller.api.clients.values()
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)
if webhook["local_only"] in (True, None) and not isinstance(request, MockRequest):
if TYPE_CHECKING:
assert isinstance(request, Request)
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 has_cloud := "cloud" in hass.config.components:
from hass_nabucasa import remote # pylint: disable=import-outside-toplevel
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)
if webhook["local_only"]:
return Response(status=HTTPStatus.OK)

View File

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

View File

@ -17,7 +17,7 @@
"iot_class": "local_push",
"loggers": ["async_upnp_client", "yeelight"],
"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": [
{
"type": "_miio._udp.local.",

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@
AEMET-OpenData==0.2.2
# homeassistant.components.aladdin_connect
AIOAladdinConnect==0.1.56
AIOAladdinConnect==0.1.57
# homeassistant.components.honeywell
AIOSomecomfort==0.0.15
@ -69,7 +69,7 @@ PyMetno==0.10.0
PyMicroBot==0.0.9
# homeassistant.components.nina
PyNINA==0.3.1
PyNINA==0.3.2
# homeassistant.components.mobile_app
# homeassistant.components.owntracks
@ -172,7 +172,7 @@ aioairq==0.2.4
aioairzone-cloud==0.2.1
# homeassistant.components.airzone
aioairzone==0.6.4
aioairzone==0.6.5
# homeassistant.components.ambient_station
aioambient==2023.04.0
@ -227,7 +227,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10
# homeassistant.components.homekit_controller
aiohomekit==2.6.12
aiohomekit==2.6.15
# homeassistant.components.emulated_hue
# homeassistant.components.http
@ -335,7 +335,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5
# homeassistant.components.unifi
aiounifi==51
aiounifi==52
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
@ -424,10 +424,10 @@ base36==0.1.1
beautifulsoup4==4.11.1
# homeassistant.components.zha
bellows==0.35.8
bellows==0.35.9
# homeassistant.components.bmw_connected_drive
bimmer-connected==0.13.8
bimmer-connected==0.13.9
# homeassistant.components.bluetooth
bleak-retry-connector==3.1.1
@ -1037,7 +1037,7 @@ openerz-api==0.2.0
openhomedevice==2.2.0
# homeassistant.components.opower
opower==0.0.20
opower==0.0.26
# homeassistant.components.oralb
oralb-ble==0.17.6
@ -1169,7 +1169,7 @@ pyairnow==1.2.1
# homeassistant.components.airvisual
# homeassistant.components.airvisual_pro
pyairvisual==2022.12.1
pyairvisual==2023.08.1
# homeassistant.components.atag
pyatag==0.3.5.3
@ -1178,7 +1178,7 @@ pyatag==0.3.5.3
pyatmo==7.5.0
# homeassistant.components.apple_tv
pyatv==0.13.3
pyatv==0.13.4
# homeassistant.components.aussie_broadband
pyaussiebb==0.0.15
@ -1352,7 +1352,7 @@ pymailgunner==1.4
pymata-express==1.19
# homeassistant.components.mazda
pymazda==0.3.10
pymazda==0.3.11
# homeassistant.components.melcloud
pymelcloud==2.5.8
@ -1388,7 +1388,7 @@ pynuki==1.6.2
pynut2==2.1.2
# homeassistant.components.nws
pynws==1.5.0
pynws==1.5.1
# homeassistant.components.nx584
pynx584==0.5
@ -1474,7 +1474,7 @@ pyrympro==0.0.7
pysabnzbd==1.1.1
# homeassistant.components.sensibo
pysensibo==1.0.32
pysensibo==1.0.33
# homeassistant.components.serial
# homeassistant.components.zha
@ -1579,7 +1579,7 @@ python-picnic-api==1.1.0
python-qbittorrent==0.4.3
# homeassistant.components.roborock
python-roborock==0.31.1
python-roborock==0.32.2
# homeassistant.components.smarttub
python-smarttub==0.0.33
@ -1588,7 +1588,7 @@ python-smarttub==0.0.33
python-songpal==0.15.2
# homeassistant.components.tado
python-tado==0.15.0
python-tado==0.16.0
# homeassistant.components.telegram_bot
python-telegram-bot==13.1
@ -1869,7 +1869,7 @@ toonapi==0.2.1
total-connect-client==2023.2
# homeassistant.components.tplink_omada
tplink_omada_client==1.2.4
tplink_omada_client==1.3.2
# homeassistant.components.transmission
transmission-rpc==4.1.5
@ -2007,7 +2007,7 @@ yalexs-ble==2.2.3
yalexs==1.5.1
# homeassistant.components.yeelight
yeelight==0.7.12
yeelight==0.7.13
# homeassistant.components.yolink
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)
HVAC_MOCK = {
HVAC_MOCK_1 = {
API_DATA: [
{
API_SYSTEM_ID: 1,
@ -340,7 +340,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None:
}
with patch(
"homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
return_value=HVAC_MOCK,
return_value=HVAC_MOCK_1,
):
await hass.services.async_call(
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")
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:
"""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"
DONE_MESSAGE = "alert_gone"
NOTIFIER = "test"
BAD_NOTIFIER = "bad_notifier"
TEMPLATE = "{{ states.sensor.test.entity_id }}"
TEST_ENTITY = "sensor.test"
TITLE = "{{ states.sensor.test.entity_id }}"
@ -199,6 +200,26 @@ async def test_notification(
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(
hass: HomeAssistant, mock_notifier: list[ServiceCall]
) -> None:

View File

@ -90,6 +90,7 @@ async def test_user_flow_cannot_connect(
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())
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 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_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 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))
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("reason") == "already_configured"
mock_api.async_generate_cert_if_missing.assert_called()
mock_api.async_get_name_and_mac.assert_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 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))
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("reason") == "already_configured"
mock_api.async_generate_cert_if_missing.assert_called()
mock_api.async_get_name_and_mac.assert_called()
mock_api.async_start_pairing.assert_not_called()

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
from datetime import timedelta
import subprocess
from typing import Any
from unittest.mock import patch
@ -16,7 +17,7 @@ from homeassistant.components.homeassistant import (
SERVICE_UPDATE_ENTITY,
)
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.helpers import entity_registry as er
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")
assert entity_state
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',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': 'unavailable',
})
# ---
# 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 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
helper.pairing.testing.update_named_service(
"Button 1", {CharacteristicsTypes.INPUT_EVENT: 1}

View File

@ -67,6 +67,10 @@ async def setup_entry(
return_value=PROP,
), patch(
"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, {})
await hass.async_block_till_done()

View File

@ -547,6 +547,163 @@
"autoOffEnabled": false,
"antiMoldTimer": 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(
hass: HomeAssistant, load_int: ConfigEntry, get_data: SensiboData
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
get_data: SensiboData,
load_int: ConfigEntry,
) -> None:
"""Test the Sensibo climate."""
state1 = hass.states.get("climate.hallway")
state2 = hass.states.get("climate.kitchen")
state3 = hass.states.get("climate.bedroom")
assert state1.state == "heat"
assert state1.attributes == {
@ -113,6 +117,19 @@ async def test_climate(
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(
hass: HomeAssistant,

View File

@ -470,6 +470,7 @@ async def test_wlan_client_sensors(
wireless_client_1 = {
"essid": "SSID 1",
"is_wired": False,
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
"mac": "00:00:00:00:00:01",
"name": "Wireless client",
"oui": "Producer",
@ -479,6 +480,7 @@ async def test_wlan_client_sensors(
wireless_client_2 = {
"essid": "SSID 2",
"is_wired": False,
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
"mac": "00:00:00:00:00:02",
"name": "Wireless client2",
"oui": "Producer2",
@ -526,9 +528,17 @@ async def test_wlan_client_sensors(
# Verify state update - decreasing number
wireless_client_1["essid"] = "SSID"
wireless_client_2["essid"] = "SSID"
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)
async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL)

View File

@ -1,7 +1,7 @@
"""Test the webhook component."""
from http import HTTPStatus
from ipaddress import ip_address
from unittest.mock import patch
from unittest.mock import Mock, patch
from aiohttp import web
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:
"""Test posting a webhook with local only."""
hass.config.components.add("cloud")
hooks = []
webhook_id = webhook.async_generate_id()
@ -234,6 +236,16 @@ async def test_webhook_local_only(hass: HomeAssistant, mock_client) -> None:
# No hook received
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(
hass: HomeAssistant,

View File

@ -1,6 +1,6 @@
"""The tests for the webhook automation trigger."""
from ipaddress import ip_address
from unittest.mock import patch
from unittest.mock import Mock, patch
import pytest
@ -68,6 +68,9 @@ async def test_webhook_post(
hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator
) -> None:
"""Test triggering with a POST webhook."""
# Set up fake cloud
hass.config.components.add("cloud")
events = []
@callback
@ -114,6 +117,16 @@ async def test_webhook_post(
await hass.async_block_till_done()
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(
hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator
@ -141,7 +154,6 @@ async def test_webhook_allowed_methods_internet(
},
"action": {
"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()
await client.post("/api/webhook/post_webhook", data={"hello": "world"})
await client.post("/api/webhook/post_webhook")
await hass.async_block_till_done()
assert len(events) == 0
@ -160,7 +172,7 @@ async def test_webhook_allowed_methods_internet(
"homeassistant.components.webhook.ip_address",
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()
assert len(events) == 1