This commit is contained in:
Franck Nijhof 2023-08-18 15:44:40 +02:00 committed by GitHub
commit bdd202b873
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 492 additions and 224 deletions

View File

@ -11,7 +11,7 @@ DEFAULT_ALLOW_SERVICE_CALLS = True
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
STABLE_BLE_VERSION_STR = "2023.6.0" STABLE_BLE_VERSION_STR = "2023.8.0"
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
PROJECT_URLS = { PROJECT_URLS = {
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",

View File

@ -54,5 +54,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["flux_led"], "loggers": ["flux_led"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["flux-led==1.0.1"] "requirements": ["flux-led==1.0.2"]
} }

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230802.0"] "requirements": ["home-assistant-frontend==20230802.1"]
} }

View File

@ -113,9 +113,10 @@ class GoGoGate2Entity(CoordinatorEntity[DeviceDataUpdateCoordinator]):
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Device info for the controller.""" """Device info for the controller."""
data = self.coordinator.data data = self.coordinator.data
configuration_url = ( if data.remoteaccessenabled:
f"https://{data.remoteaccess}" if data.remoteaccess else None configuration_url = f"https://{data.remoteaccess}"
) else:
configuration_url = f"http://{self._config_entry.data[CONF_IP_ADDRESS]}"
return DeviceInfo( return DeviceInfo(
configuration_url=configuration_url, configuration_url=configuration_url,
identifiers={(DOMAIN, str(self._config_entry.unique_id))}, identifiers={(DOMAIN, str(self._config_entry.unique_id))},

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.15"], "requirements": ["aiohomekit==2.6.16"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
} }

View File

@ -146,13 +146,13 @@ class HoneywellUSThermostat(ClimateEntity):
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
) )
if device._data["canControlHumidification"]: if device._data.get("canControlHumidification"):
self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY
if device.raw_ui_data["SwitchEmergencyHeatAllowed"]: if device.raw_ui_data.get("SwitchEmergencyHeatAllowed"):
self._attr_supported_features |= ClimateEntityFeature.AUX_HEAT self._attr_supported_features |= ClimateEntityFeature.AUX_HEAT
if not device._data["hasFan"]: if not device._data.get("hasFan"):
return return
# not all honeywell fans support all modes # not all honeywell fans support all modes

View File

@ -20,6 +20,7 @@ from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from . import HoneywellData
from .const import DOMAIN, HUMIDITY_STATUS_KEY, TEMPERATURE_STATUS_KEY from .const import DOMAIN, HUMIDITY_STATUS_KEY, TEMPERATURE_STATUS_KEY
@ -71,7 +72,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Honeywell thermostat.""" """Set up the Honeywell thermostat."""
data = hass.data[DOMAIN][config_entry.entry_id] data: HoneywellData = hass.data[DOMAIN][config_entry.entry_id]
sensors = [] sensors = []
for device in data.devices.values(): for device in data.devices.values():

View File

@ -8,6 +8,6 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["deepmerge", "pyipp"], "loggers": ["deepmerge", "pyipp"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pyipp==0.14.2"], "requirements": ["pyipp==0.14.3"],
"zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."]
} }

View File

@ -21,7 +21,12 @@ from homeassistant.components.climate import (
HVACMode, HVACMode,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE from homeassistant.const import (
ATTR_TEMPERATURE,
PRECISION_HALVES,
PRECISION_WHOLE,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
@ -113,7 +118,6 @@ async def async_setup_entry(
), ),
location, location,
device, device,
hass.config.units.temperature_unit,
) )
) )
@ -140,10 +144,15 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
description: ClimateEntityDescription, description: ClimateEntityDescription,
location: LyricLocation, location: LyricLocation,
device: LyricDevice, device: LyricDevice,
temperature_unit: str,
) -> None: ) -> None:
"""Initialize Honeywell Lyric climate entity.""" """Initialize Honeywell Lyric climate entity."""
self._temperature_unit = temperature_unit # Use the native temperature unit from the device settings
if device.units == "Fahrenheit":
self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
self._attr_precision = PRECISION_WHOLE
else:
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_precision = PRECISION_HALVES
# Setup supported hvac modes # Setup supported hvac modes
self._attr_hvac_modes = [HVACMode.OFF] self._attr_hvac_modes = [HVACMode.OFF]
@ -176,11 +185,6 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
return SUPPORT_FLAGS_LCC return SUPPORT_FLAGS_LCC
return SUPPORT_FLAGS_TCC return SUPPORT_FLAGS_TCC
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return self._temperature_unit
@property @property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
"""Return the current temperature.""" """Return the current temperature."""

View File

@ -17,7 +17,7 @@ from homeassistant.components.sensor import (
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE from homeassistant.const import PERCENTAGE, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
@ -76,6 +76,11 @@ async def async_setup_entry(
for location in coordinator.data.locations: for location in coordinator.data.locations:
for device in location.devices: for device in location.devices:
if device.indoorTemperature: if device.indoorTemperature:
if device.units == "Fahrenheit":
native_temperature_unit = UnitOfTemperature.FAHRENHEIT
else:
native_temperature_unit = UnitOfTemperature.CELSIUS
entities.append( entities.append(
LyricSensor( LyricSensor(
coordinator, coordinator,
@ -84,7 +89,7 @@ async def async_setup_entry(
name="Indoor Temperature", name="Indoor Temperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=hass.config.units.temperature_unit, native_unit_of_measurement=native_temperature_unit,
value=lambda device: device.indoorTemperature, value=lambda device: device.indoorTemperature,
), ),
location, location,
@ -108,6 +113,11 @@ async def async_setup_entry(
) )
) )
if device.outdoorTemperature: if device.outdoorTemperature:
if device.units == "Fahrenheit":
native_temperature_unit = UnitOfTemperature.FAHRENHEIT
else:
native_temperature_unit = UnitOfTemperature.CELSIUS
entities.append( entities.append(
LyricSensor( LyricSensor(
coordinator, coordinator,
@ -116,7 +126,7 @@ async def async_setup_entry(
name="Outdoor Temperature", name="Outdoor Temperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=hass.config.units.temperature_unit, native_unit_of_measurement=native_temperature_unit,
value=lambda device: device.outdoorTemperature, value=lambda device: device.outdoorTemperature,
), ),
location, location,

View File

@ -48,10 +48,12 @@ from .const import (
CONF_MIN_VALUE, CONF_MIN_VALUE,
CONF_PRECISION, CONF_PRECISION,
CONF_SCALE, CONF_SCALE,
CONF_SLAVE_COUNT,
CONF_STATE_OFF, CONF_STATE_OFF,
CONF_STATE_ON, CONF_STATE_ON,
CONF_SWAP, CONF_SWAP,
CONF_SWAP_BYTE, CONF_SWAP_BYTE,
CONF_SWAP_NONE,
CONF_SWAP_WORD, CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD_BYTE,
CONF_VERIFY, CONF_VERIFY,
@ -155,15 +157,25 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
"""Initialize the switch.""" """Initialize the switch."""
super().__init__(hub, config) super().__init__(hub, config)
self._swap = config[CONF_SWAP] self._swap = config[CONF_SWAP]
if self._swap == CONF_SWAP_NONE:
self._swap = None
self._data_type = config[CONF_DATA_TYPE] self._data_type = config[CONF_DATA_TYPE]
self._structure: str = config[CONF_STRUCTURE] self._structure: str = config[CONF_STRUCTURE]
self._precision = config[CONF_PRECISION] self._precision = config[CONF_PRECISION]
self._scale = config[CONF_SCALE] self._scale = config[CONF_SCALE]
self._offset = config[CONF_OFFSET] self._offset = config[CONF_OFFSET]
self._count = config[CONF_COUNT] self._slave_count = config.get(CONF_SLAVE_COUNT, 0)
self._slave_size = self._count = config[CONF_COUNT]
def _swap_registers(self, registers: list[int]) -> list[int]: def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]:
"""Do swap as needed.""" """Do swap as needed."""
if slave_count:
swapped = []
for i in range(0, self._slave_count + 1):
inx = i * self._slave_size
inx2 = inx + self._slave_size
swapped.extend(self._swap_registers(registers[inx:inx2], 0))
return swapped
if self._swap in (CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE): if self._swap in (CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE):
# convert [12][34] --> [21][43] # convert [12][34] --> [21][43]
for i, register in enumerate(registers): for i, register in enumerate(registers):
@ -191,7 +203,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
def unpack_structure_result(self, registers: list[int]) -> str | None: def unpack_structure_result(self, registers: list[int]) -> str | None:
"""Convert registers to proper result.""" """Convert registers to proper result."""
registers = self._swap_registers(registers) if self._swap:
registers = self._swap_registers(registers, self._slave_count)
byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers])
if self._data_type == DataType.STRING: if self._data_type == DataType.STRING:
return byte_string.decode() return byte_string.decode()

View File

@ -206,7 +206,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
int.from_bytes(as_bytes[i : i + 2], "big") int.from_bytes(as_bytes[i : i + 2], "big")
for i in range(0, len(as_bytes), 2) for i in range(0, len(as_bytes), 2)
] ]
registers = self._swap_registers(raw_regs) registers = self._swap_registers(raw_regs, 0)
if self._data_type in ( if self._data_type in (
DataType.INT16, DataType.INT16,

View File

@ -68,7 +68,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
"""Initialize the modbus register sensor.""" """Initialize the modbus register sensor."""
super().__init__(hub, entry) super().__init__(hub, entry)
if slave_count: if slave_count:
self._count = self._count * slave_count self._count = self._count * (slave_count + 1)
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)
@ -132,10 +132,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
self._coordinator.async_set_updated_data(None) self._coordinator.async_set_updated_data(None)
else: else:
self._attr_native_value = result self._attr_native_value = result
if self._attr_native_value is None: self._attr_available = self._attr_native_value is not None
self._attr_available = False
else:
self._attr_available = True
self._lazy_errors = self._lazy_error_count self._lazy_errors = self._lazy_error_count
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -65,25 +65,25 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]:
name = config[CONF_NAME] name = config[CONF_NAME]
structure = config.get(CONF_STRUCTURE) structure = config.get(CONF_STRUCTURE)
slave_count = config.get(CONF_SLAVE_COUNT, 0) + 1 slave_count = config.get(CONF_SLAVE_COUNT, 0) + 1
swap_type = config.get(CONF_SWAP) slave = config.get(CONF_SLAVE, 0)
swap_type = config.get(CONF_SWAP, CONF_SWAP_NONE)
if (
slave_count > 1
and count > 1
and data_type not in (DataType.CUSTOM, DataType.STRING)
):
error = f"{name} {CONF_COUNT} cannot be mixed with {data_type}"
raise vol.Invalid(error)
if config[CONF_DATA_TYPE] != DataType.CUSTOM: if config[CONF_DATA_TYPE] != DataType.CUSTOM:
if structure: if structure:
error = f"{name} structure: cannot be mixed with {data_type}" error = f"{name} structure: cannot be mixed with {data_type}"
raise vol.Invalid(error)
if data_type not in DEFAULT_STRUCT_FORMAT:
error = f"Error in sensor {name}. data_type `{data_type}` not supported"
raise vol.Invalid(error)
structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" if config[CONF_DATA_TYPE] == DataType.CUSTOM:
if CONF_COUNT not in config: if slave or slave_count > 1:
config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count error = f"{name}: `{CONF_STRUCTURE}` illegal with `{CONF_SLAVE_COUNT}` / `{CONF_SLAVE}`"
if slave_count > 1: raise vol.Invalid(error)
structure = f">{slave_count}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" if swap_type != CONF_SWAP_NONE:
else: error = f"{name}: `{CONF_STRUCTURE}` illegal with `{CONF_SWAP}`"
structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}"
else:
if slave_count > 1:
error = f"{name} structure: cannot be mixed with {CONF_SLAVE_COUNT}"
raise vol.Invalid(error) raise vol.Invalid(error)
if not structure: if not structure:
error = ( error = (
@ -102,19 +102,37 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]:
f"Structure request {size} bytes, " f"Structure request {size} bytes, "
f"but {count} registers have a size of {bytecount} bytes" f"but {count} registers have a size of {bytecount} bytes"
) )
return {
**config,
CONF_STRUCTURE: structure,
CONF_SWAP: swap_type,
}
if data_type not in DEFAULT_STRUCT_FORMAT:
error = f"Error in sensor {name}. data_type `{data_type}` not supported"
raise vol.Invalid(error)
if slave_count > 1 and data_type == DataType.STRING:
error = f"{name}: `{data_type}` illegal with `{CONF_SLAVE_COUNT}`"
raise vol.Invalid(error)
if swap_type != CONF_SWAP_NONE: if CONF_COUNT not in config:
if swap_type == CONF_SWAP_BYTE: config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count
regs_needed = 1 if swap_type != CONF_SWAP_NONE:
else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD if swap_type == CONF_SWAP_BYTE:
regs_needed = 2 regs_needed = 1
if count < regs_needed or (count % regs_needed) != 0: else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD
raise vol.Invalid( regs_needed = 2
f"Error in sensor {name} swap({swap_type}) " count = config[CONF_COUNT]
"not possible due to the registers " if count < regs_needed or (count % regs_needed) != 0:
f"count: {count}, needed: {regs_needed}" raise vol.Invalid(
) f"Error in sensor {name} swap({swap_type}) "
"not possible due to the registers "
f"count: {count}, needed: {regs_needed}"
)
structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}"
if slave_count > 1:
structure = f">{slave_count}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}"
else:
structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}"
return { return {
**config, **config,
CONF_STRUCTURE: structure, CONF_STRUCTURE: structure,

View File

@ -3,7 +3,7 @@ from collections import namedtuple
import datetime import datetime
import logging import logging
from nessclient import ArmingState, Client from nessclient import ArmingMode, ArmingState, Client
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
@ -136,9 +136,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, SIGNAL_ZONE_CHANGED, ZoneChangedData(zone_id=zone_id, state=state) hass, SIGNAL_ZONE_CHANGED, ZoneChangedData(zone_id=zone_id, state=state)
) )
def on_state_change(arming_state: ArmingState): def on_state_change(arming_state: ArmingState, arming_mode: ArmingMode | None):
"""Receives and propagates arming state updates.""" """Receives and propagates arming state updates."""
async_dispatcher_send(hass, SIGNAL_ARMING_STATE_CHANGED, arming_state) async_dispatcher_send(
hass, SIGNAL_ARMING_STATE_CHANGED, arming_state, arming_mode
)
client.on_zone_change(on_zone_change) client.on_zone_change(on_zone_change)
client.on_state_change(on_state_change) client.on_state_change(on_state_change)

View File

@ -3,12 +3,15 @@ from __future__ import annotations
import logging import logging
from nessclient import ArmingState, Client from nessclient import ArmingMode, ArmingState, Client
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_VACATION,
STATE_ALARM_ARMING, STATE_ALARM_ARMING,
STATE_ALARM_DISARMED, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_PENDING,
@ -23,6 +26,15 @@ from . import DATA_NESS, SIGNAL_ARMING_STATE_CHANGED
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ARMING_MODE_TO_STATE = {
ArmingMode.ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
ArmingMode.ARMED_HOME: STATE_ALARM_ARMED_HOME,
ArmingMode.ARMED_DAY: STATE_ALARM_ARMED_AWAY, # no applicable state, fallback to away
ArmingMode.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT,
ArmingMode.ARMED_VACATION: STATE_ALARM_ARMED_VACATION,
ArmingMode.ARMED_HIGHEST: STATE_ALARM_ARMED_AWAY, # no applicable state, fallback to away
}
async def async_setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
@ -79,7 +91,9 @@ class NessAlarmPanel(alarm.AlarmControlPanelEntity):
await self._client.panic(code) await self._client.panic(code)
@callback @callback
def _handle_arming_state_change(self, arming_state: ArmingState) -> None: def _handle_arming_state_change(
self, arming_state: ArmingState, arming_mode: ArmingMode | None
) -> None:
"""Handle arming state update.""" """Handle arming state update."""
if arming_state == ArmingState.UNKNOWN: if arming_state == ArmingState.UNKNOWN:
@ -91,7 +105,9 @@ class NessAlarmPanel(alarm.AlarmControlPanelEntity):
elif arming_state == ArmingState.EXIT_DELAY: elif arming_state == ArmingState.EXIT_DELAY:
self._attr_state = STATE_ALARM_ARMING self._attr_state = STATE_ALARM_ARMING
elif arming_state == ArmingState.ARMED: elif arming_state == ArmingState.ARMED:
self._attr_state = STATE_ALARM_ARMED_AWAY self._attr_state = ARMING_MODE_TO_STATE.get(
arming_mode, STATE_ALARM_ARMED_AWAY
)
elif arming_state == ArmingState.ENTRY_DELAY: elif arming_state == ArmingState.ENTRY_DELAY:
self._attr_state = STATE_ALARM_PENDING self._attr_state = STATE_ALARM_PENDING
elif arming_state == ArmingState.TRIGGERED: elif arming_state == ArmingState.TRIGGERED:

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/ness_alarm", "documentation": "https://www.home-assistant.io/integrations/ness_alarm",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["nessclient"], "loggers": ["nessclient"],
"requirements": ["nessclient==0.10.0"] "requirements": ["nessclient==1.0.0"]
} }

View File

@ -12,6 +12,7 @@ from opower import (
InvalidAuth, InvalidAuth,
MeterType, MeterType,
Opower, Opower,
ReadResolution,
) )
from homeassistant.components.recorder import get_instance from homeassistant.components.recorder import get_instance
@ -177,44 +178,55 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
"""Get all cost reads since account activation but at different resolutions depending on age. """Get all cost reads since account activation but at different resolutions depending on age.
- month resolution for all years (since account activation) - month resolution for all years (since account activation)
- day resolution for past 3 years - day resolution for past 3 years (if account's read resolution supports it)
- hour resolution for past 2 months, only for electricity, not gas - hour resolution for past 2 months (if account's read resolution supports it)
""" """
cost_reads = [] cost_reads = []
start = None start = None
end = datetime.now() - timedelta(days=3 * 365) end = datetime.now()
if account.read_resolution != ReadResolution.BILLING:
end -= timedelta(days=3 * 365)
cost_reads += await self.api.async_get_cost_reads( cost_reads += await self.api.async_get_cost_reads(
account, AggregateType.BILL, start, end account, AggregateType.BILL, start, end
) )
if account.read_resolution == ReadResolution.BILLING:
return cost_reads
start = end if not cost_reads else cost_reads[-1].end_time start = end if not cost_reads else cost_reads[-1].end_time
end = ( end = datetime.now()
datetime.now() - timedelta(days=2 * 30) if account.read_resolution != ReadResolution.DAY:
if account.meter_type == MeterType.ELEC end -= timedelta(days=2 * 30)
else datetime.now()
)
cost_reads += await self.api.async_get_cost_reads( cost_reads += await self.api.async_get_cost_reads(
account, AggregateType.DAY, start, end account, AggregateType.DAY, start, end
) )
if account.meter_type == MeterType.ELEC: if account.read_resolution == ReadResolution.DAY:
start = end if not cost_reads else cost_reads[-1].end_time return cost_reads
end = datetime.now()
cost_reads += await self.api.async_get_cost_reads( start = end if not cost_reads else cost_reads[-1].end_time
account, AggregateType.HOUR, start, end end = datetime.now()
) cost_reads += await self.api.async_get_cost_reads(
account, AggregateType.HOUR, start, end
)
return cost_reads return cost_reads
async def _async_get_recent_cost_reads( async def _async_get_recent_cost_reads(
self, account: Account, last_stat_time: float self, account: Account, last_stat_time: float
) -> list[CostRead]: ) -> list[CostRead]:
"""Get cost reads within the past 30 days to allow corrections in data from utilities. """Get cost reads within the past 30 days to allow corrections in data from utilities."""
if account.read_resolution in [
Hourly for electricity, daily for gas. ReadResolution.HOUR,
""" ReadResolution.HALF_HOUR,
ReadResolution.QUARTER_HOUR,
]:
aggregate_type = AggregateType.HOUR
elif account.read_resolution == ReadResolution.DAY:
aggregate_type = AggregateType.DAY
else:
aggregate_type = AggregateType.BILL
return await self.api.async_get_cost_reads( return await self.api.async_get_cost_reads(
account, account,
AggregateType.HOUR aggregate_type,
if account.meter_type == MeterType.ELEC
else AggregateType.DAY,
datetime.fromtimestamp(last_stat_time) - timedelta(days=30), datetime.fromtimestamp(last_stat_time) - timedelta(days=30),
datetime.now(), datetime.now(),
) )

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.26"] "requirements": ["opower==0.0.29"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/rainbird", "documentation": "https://www.home-assistant.io/integrations/rainbird",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyrainbird"], "loggers": ["pyrainbird"],
"requirements": ["pyrainbird==3.0.0"] "requirements": ["pyrainbird==4.0.0"]
} }

View File

@ -5,6 +5,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from reolink_aio.api import ( from reolink_aio.api import (
DUAL_LENS_DUAL_MOTION_MODELS,
FACE_DETECTION_TYPE, FACE_DETECTION_TYPE,
PERSON_DETECTION_TYPE, PERSON_DETECTION_TYPE,
PET_DETECTION_TYPE, PET_DETECTION_TYPE,
@ -128,6 +129,9 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt
super().__init__(reolink_data, channel) super().__init__(reolink_data, channel)
self.entity_description = entity_description self.entity_description = entity_description
if self._host.api.model in DUAL_LENS_DUAL_MOTION_MODELS:
self._attr_name = f"{entity_description.name} lens {self._channel}"
self._attr_unique_id = ( self._attr_unique_id = (
f"{self._host.unique_id}_{self._channel}_{entity_description.key}" f"{self._host.unique_id}_{self._channel}_{entity_description.key}"
) )

View File

@ -163,7 +163,7 @@ class ReolinkHost:
else: else:
_LOGGER.debug( _LOGGER.debug(
"Camera model %s most likely does not push its initial state" "Camera model %s most likely does not push its initial state"
"upon ONVIF subscription, do not check", " upon ONVIF subscription, do not check",
self._api.model, self._api.model,
) )
self._cancel_onvif_check = async_call_later( self._cancel_onvif_check = async_call_later(

View File

@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink", "documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["reolink_aio"], "loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.7.6"] "requirements": ["reolink-aio==0.7.7"]
} }

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.32.2"] "requirements": ["python-roborock==0.32.3"]
} }

View File

@ -11,7 +11,7 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["rokuecp"], "loggers": ["rokuecp"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["rokuecp==0.18.0"], "requirements": ["rokuecp==0.18.1"],
"ssdp": [ "ssdp": [
{ {
"st": "roku:ecp", "st": "roku:ecp",

View File

@ -533,7 +533,8 @@ RPC_SENSORS: Final = {
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT, device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
available=lambda status: status["n_current"] is not None, available=lambda status: (status and status["n_current"]) is not None,
removal_condition=lambda _config, status, _key: "n_current" not in status,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
"total_current": RpcSensorDescription( "total_current": RpcSensorDescription(

View File

@ -163,11 +163,12 @@ 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, None, True) self.tado = Tado(self._username, self._password)
self.tado.setDebugging(True)
# Load zones and devices # Load zones and devices
self.zones = self.tado.get_zones() self.zones = self.tado.getZones()
self.devices = self.tado.get_devices() self.devices = self.tado.getDevices()
tado_home = self.tado.get_me()["homes"][0] tado_home = self.tado.getMe()["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"]
@ -180,7 +181,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.get_devices() devices = self.tado.getDevices()
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)
@ -189,7 +190,7 @@ class TadoConnector:
INSIDE_TEMPERATURE_MEASUREMENT INSIDE_TEMPERATURE_MEASUREMENT
in device["characteristics"]["capabilities"] in device["characteristics"]["capabilities"]
): ):
device[TEMP_OFFSET] = self.tado.get_device_info( device[TEMP_OFFSET] = self.tado.getDeviceInfo(
device_short_serial_no, TEMP_OFFSET device_short_serial_no, TEMP_OFFSET
) )
except RuntimeError: except RuntimeError:
@ -217,7 +218,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.get_zone_states()["zoneStates"] zone_states = self.tado.getZoneStates()["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
@ -229,7 +230,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.get_zone_state(zone_id) data = self.tado.getZoneState(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
@ -250,8 +251,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.get_weather() self.data["weather"] = self.tado.getWeather()
self.data["geofence"] = self.tado.get_home_state() self.data["geofence"] = self.tado.getHomeState()
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"),
@ -264,15 +265,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.get_capabilities(zone_id) return self.tado.getCapabilities(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.get_auto_geofencing_supported() return self.tado.getAutoGeofencingSupported()
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.reset_zone_overlay(zone_id) self.tado.resetZoneOverlay(zone_id)
self.update_zone(zone_id) self.update_zone(zone_id)
def set_presence( def set_presence(
@ -281,11 +282,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.set_away() self.tado.setAway()
elif presence == PRESET_HOME: elif presence == PRESET_HOME:
self.tado.set_home() self.tado.setHome()
elif presence == PRESET_AUTO: elif presence == PRESET_AUTO:
self.tado.set_auto() self.tado.setAuto()
# Update everything when changing modes # Update everything when changing modes
self.update_zones() self.update_zones()
@ -319,7 +320,7 @@ class TadoConnector:
) )
try: try:
self.tado.set_zone_overlay( self.tado.setZoneOverlay(
zone_id, zone_id,
overlay_mode, overlay_mode,
temperature, temperature,
@ -339,7 +340,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.set_zone_overlay( self.tado.setZoneOverlay(
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:
@ -350,6 +351,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.set_temp_offset(device_id, offset) self.tado.setTempOffset(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.16.0"] "requirements": ["python-tado==0.15.0"]
} }

View File

@ -50,7 +50,6 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT, native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER, device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
name="Current Consumption",
emeter_attr="power", emeter_attr="power",
precision=1, precision=1,
), ),
@ -60,7 +59,6 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
name="Total Consumption",
emeter_attr="total", emeter_attr="total",
precision=3, precision=3,
), ),
@ -70,7 +68,6 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
name="Today's Consumption",
precision=3, precision=3,
), ),
TPLinkSensorEntityDescription( TPLinkSensorEntityDescription(

View File

@ -20,16 +20,19 @@ ENTITY_LEGACY_PROVIDER_GROUP = "entity_or_legacy_provider"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = vol.All(
{ cv.has_at_least_one_key(CONF_TTS_SERVICE, CONF_ENTITY_ID),
vol.Required(CONF_NAME): cv.string, PLATFORM_SCHEMA.extend(
vol.Exclusive(CONF_TTS_SERVICE, ENTITY_LEGACY_PROVIDER_GROUP): cv.entity_id, {
vol.Exclusive(CONF_ENTITY_ID, ENTITY_LEGACY_PROVIDER_GROUP): cv.entities_domain( vol.Required(CONF_NAME): cv.string,
DOMAIN vol.Exclusive(CONF_TTS_SERVICE, ENTITY_LEGACY_PROVIDER_GROUP): cv.entity_id,
), vol.Exclusive(
vol.Required(CONF_MEDIA_PLAYER): cv.entity_id, CONF_ENTITY_ID, ENTITY_LEGACY_PROVIDER_GROUP
vol.Optional(ATTR_LANGUAGE): cv.string, ): cv.entities_domain(DOMAIN),
} vol.Required(CONF_MEDIA_PLAYER): cv.entity_id,
vol.Optional(ATTR_LANGUAGE): cv.string,
}
),
) )

View File

@ -83,13 +83,16 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
raise UpdateFailed("Could not read overview") from err raise UpdateFailed("Could not read overview") from err
def unpack(overview: list, value: str) -> dict | list: def unpack(overview: list, value: str) -> dict | list:
return next( return (
( next(
item["data"]["installation"][value] (
for item in overview item["data"]["installation"][value]
if value in item.get("data", {}).get("installation", {}) for item in overview
), if value in item.get("data", {}).get("installation", {})
[], ),
[],
)
or []
) )
# Store data in a way Home Assistant can easily consume it # Store data in a way Home Assistant can easily consume it

View File

@ -438,22 +438,16 @@ class DataManager:
async def async_get_sleep_summary(self) -> dict[Measurement, Any]: async def async_get_sleep_summary(self) -> dict[Measurement, Any]:
"""Get the sleep summary data.""" """Get the sleep summary data."""
_LOGGER.debug("Updating withing sleep summary") _LOGGER.debug("Updating withing sleep summary")
now = dt_util.utcnow() now = dt_util.now()
yesterday = now - datetime.timedelta(days=1) yesterday = now - datetime.timedelta(days=1)
yesterday_noon = datetime.datetime( yesterday_noon = dt_util.start_of_local_day(yesterday) + datetime.timedelta(
yesterday.year, hours=12
yesterday.month,
yesterday.day,
12,
0,
0,
0,
datetime.UTC,
) )
yesterday_noon_utc = dt_util.as_utc(yesterday_noon)
def get_sleep_summary() -> SleepGetSummaryResponse: def get_sleep_summary() -> SleepGetSummaryResponse:
return self._api.sleep_get_summary( return self._api.sleep_get_summary(
lastupdate=yesterday_noon, lastupdate=yesterday_noon_utc,
data_fields=[ data_fields=[
GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY, GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY,
GetSleepSummaryField.DEEP_SLEEP_DURATION, GetSleepSummaryField.DEEP_SLEEP_DURATION,

View File

@ -238,7 +238,7 @@ def _parse_custom_effects(effects_config) -> dict[str, dict[str, Any]]:
def _async_cmd(func): def _async_cmd(func):
"""Define a wrapper to catch exceptions from the bulb.""" """Define a wrapper to catch exceptions from the bulb."""
async def _async_wrap(self: YeelightGenericLight, *args, **kwargs): async def _async_wrap(self: YeelightBaseLight, *args, **kwargs):
for attempts in range(2): for attempts in range(2):
try: try:
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs) _LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
@ -403,8 +403,8 @@ def _async_setup_services(hass: HomeAssistant):
) )
class YeelightGenericLight(YeelightEntity, LightEntity): class YeelightBaseLight(YeelightEntity, LightEntity):
"""Representation of a Yeelight generic light.""" """Abstract Yeelight light."""
_attr_color_mode = ColorMode.BRIGHTNESS _attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
@ -861,7 +861,13 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
await self._bulb.async_set_scene(scene_class, *args) await self._bulb.async_set_scene(scene_class, *args)
class YeelightColorLightSupport(YeelightGenericLight): class YeelightGenericLight(YeelightBaseLight):
"""Representation of a generic Yeelight."""
_attr_name = None
class YeelightColorLightSupport(YeelightBaseLight):
"""Representation of a Color Yeelight light support.""" """Representation of a Color Yeelight light support."""
_attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS, ColorMode.RGB} _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS, ColorMode.RGB}
@ -884,7 +890,7 @@ class YeelightColorLightSupport(YeelightGenericLight):
return YEELIGHT_COLOR_EFFECT_LIST return YEELIGHT_COLOR_EFFECT_LIST
class YeelightWhiteTempLightSupport(YeelightGenericLight): class YeelightWhiteTempLightSupport(YeelightBaseLight):
"""Representation of a White temp Yeelight light.""" """Representation of a White temp Yeelight light."""
_attr_name = None _attr_name = None
@ -904,7 +910,7 @@ class YeelightNightLightSupport:
return PowerMode.NORMAL return PowerMode.NORMAL
class YeelightWithoutNightlightSwitchMixIn(YeelightGenericLight): class YeelightWithoutNightlightSwitchMixIn(YeelightBaseLight):
"""A mix-in for yeelights without a nightlight switch.""" """A mix-in for yeelights without a nightlight switch."""
@property @property
@ -940,7 +946,7 @@ class YeelightColorLightWithoutNightlightSwitchLight(
class YeelightColorLightWithNightlightSwitch( class YeelightColorLightWithNightlightSwitch(
YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight YeelightNightLightSupport, YeelightColorLightSupport, YeelightBaseLight
): ):
"""Representation of a Yeelight with rgb support and nightlight. """Representation of a Yeelight with rgb support and nightlight.
@ -964,7 +970,7 @@ class YeelightWhiteTempWithoutNightlightSwitch(
class YeelightWithNightLight( class YeelightWithNightLight(
YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightGenericLight YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightBaseLight
): ):
"""Representation of a Yeelight with temp only support and nightlight. """Representation of a Yeelight with temp only support and nightlight.
@ -979,7 +985,7 @@ class YeelightWithNightLight(
return super().is_on and not self.device.is_nightlight_enabled return super().is_on and not self.device.is_nightlight_enabled
class YeelightNightLightMode(YeelightGenericLight): class YeelightNightLightMode(YeelightBaseLight):
"""Representation of a Yeelight when in nightlight mode.""" """Representation of a Yeelight when in nightlight mode."""
_attr_color_mode = ColorMode.BRIGHTNESS _attr_color_mode = ColorMode.BRIGHTNESS

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 = "2" PATCH_VERSION: Final = "3"
__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

@ -22,7 +22,7 @@ ha-av==10.1.1
hass-nabucasa==0.69.0 hass-nabucasa==0.69.0
hassil==1.2.5 hassil==1.2.5
home-assistant-bluetooth==1.10.2 home-assistant-bluetooth==1.10.2
home-assistant-frontend==20230802.0 home-assistant-frontend==20230802.1
home-assistant-intents==2023.8.2 home-assistant-intents==2023.8.2
httpx==0.24.1 httpx==0.24.1
ifaddr==0.2.0 ifaddr==0.2.0

View File

@ -1,10 +1,10 @@
[build-system] [build-system]
requires = ["setuptools~=68.0", "wheel~=0.40.0"] requires = ["setuptools==68.0.0", "wheel~=0.40.0"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.8.2" version = "2023.8.3"
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

@ -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.15 aiohomekit==2.6.16
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@ -800,7 +800,7 @@ fjaraskupan==2.2.0
flipr-api==1.5.0 flipr-api==1.5.0
# homeassistant.components.flux_led # homeassistant.components.flux_led
flux-led==1.0.1 flux-led==1.0.2
# homeassistant.components.homekit # homeassistant.components.homekit
# homeassistant.components.recorder # homeassistant.components.recorder
@ -988,7 +988,7 @@ hole==0.8.0
holidays==0.28 holidays==0.28
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20230802.0 home-assistant-frontend==20230802.1
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.8.2 home-assistant-intents==2023.8.2
@ -1243,7 +1243,7 @@ nad-receiver==0.3.0
ndms2-client==0.1.2 ndms2-client==0.1.2
# homeassistant.components.ness_alarm # homeassistant.components.ness_alarm
nessclient==0.10.0 nessclient==1.0.0
# homeassistant.components.netdata # homeassistant.components.netdata
netdata==1.1.0 netdata==1.1.0
@ -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.26 opower==0.0.29
# homeassistant.components.oralb # homeassistant.components.oralb
oralb-ble==0.17.6 oralb-ble==0.17.6
@ -1746,7 +1746,7 @@ pyintesishome==1.8.0
pyipma==3.0.6 pyipma==3.0.6
# homeassistant.components.ipp # homeassistant.components.ipp
pyipp==0.14.2 pyipp==0.14.3
# homeassistant.components.iqvia # homeassistant.components.iqvia
pyiqvia==2022.04.0 pyiqvia==2022.04.0
@ -1952,7 +1952,7 @@ pyqwikswitch==0.93
pyrail==0.0.3 pyrail==0.0.3
# homeassistant.components.rainbird # homeassistant.components.rainbird
pyrainbird==3.0.0 pyrainbird==4.0.0
# homeassistant.components.recswitch # homeassistant.components.recswitch
pyrecswitch==1.0.2 pyrecswitch==1.0.2
@ -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.32.2 python-roborock==0.32.3
# 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.16.0 python-tado==0.15.0
# homeassistant.components.telegram_bot # homeassistant.components.telegram_bot
python-telegram-bot==13.1 python-telegram-bot==13.1
@ -2278,7 +2278,7 @@ renault-api==0.1.13
renson-endura-delta==1.5.0 renson-endura-delta==1.5.0
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.7.6 reolink-aio==0.7.7
# homeassistant.components.idteck_prox # homeassistant.components.idteck_prox
rfk101py==0.0.1 rfk101py==0.0.1
@ -2299,7 +2299,7 @@ rjpl==0.3.6
rocketchat-API==0.6.1 rocketchat-API==0.6.1
# homeassistant.components.roku # homeassistant.components.roku
rokuecp==0.18.0 rokuecp==0.18.1
# homeassistant.components.roomba # homeassistant.components.roomba
roombapy==1.6.8 roombapy==1.6.8

View File

@ -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.15 aiohomekit==2.6.16
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@ -628,7 +628,7 @@ fjaraskupan==2.2.0
flipr-api==1.5.0 flipr-api==1.5.0
# homeassistant.components.flux_led # homeassistant.components.flux_led
flux-led==1.0.1 flux-led==1.0.2
# homeassistant.components.homekit # homeassistant.components.homekit
# homeassistant.components.recorder # homeassistant.components.recorder
@ -774,7 +774,7 @@ hole==0.8.0
holidays==0.28 holidays==0.28
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20230802.0 home-assistant-frontend==20230802.1
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.8.2 home-assistant-intents==2023.8.2
@ -957,7 +957,7 @@ mutesync==0.0.1
ndms2-client==0.1.2 ndms2-client==0.1.2
# homeassistant.components.ness_alarm # homeassistant.components.ness_alarm
nessclient==0.10.0 nessclient==1.0.0
# homeassistant.components.nmap_tracker # homeassistant.components.nmap_tracker
netmap==0.7.0.2 netmap==0.7.0.2
@ -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.26 opower==0.0.29
# homeassistant.components.oralb # homeassistant.components.oralb
oralb-ble==0.17.6 oralb-ble==0.17.6
@ -1292,7 +1292,7 @@ pyinsteon==1.4.3
pyipma==3.0.6 pyipma==3.0.6
# homeassistant.components.ipp # homeassistant.components.ipp
pyipp==0.14.2 pyipp==0.14.3
# homeassistant.components.iqvia # homeassistant.components.iqvia
pyiqvia==2022.04.0 pyiqvia==2022.04.0
@ -1453,7 +1453,7 @@ pyps4-2ndscreen==1.3.1
pyqwikswitch==0.93 pyqwikswitch==0.93
# homeassistant.components.rainbird # homeassistant.components.rainbird
pyrainbird==3.0.0 pyrainbird==4.0.0
# homeassistant.components.risco # homeassistant.components.risco
pyrisco==0.5.7 pyrisco==0.5.7
@ -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.32.2 python-roborock==0.32.3
# 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.16.0 python-tado==0.15.0
# homeassistant.components.telegram_bot # homeassistant.components.telegram_bot
python-telegram-bot==13.1 python-telegram-bot==13.1
@ -1674,7 +1674,7 @@ renault-api==0.1.13
renson-endura-delta==1.5.0 renson-endura-delta==1.5.0
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.7.6 reolink-aio==0.7.7
# homeassistant.components.rflink # homeassistant.components.rflink
rflink==0.0.65 rflink==0.0.65
@ -1683,7 +1683,7 @@ rflink==0.0.65
ring-doorbell==0.7.2 ring-doorbell==0.7.2
# homeassistant.components.roku # homeassistant.components.roku
rokuecp==0.18.0 rokuecp==0.18.1
# homeassistant.components.roomba # homeassistant.components.roomba
roombapy==1.6.8 roombapy==1.6.8

View File

@ -77,7 +77,7 @@ def _mocked_ismartgate_closed_door_response():
ismartgatename="ismartgatename0", ismartgatename="ismartgatename0",
model="ismartgatePRO", model="ismartgatePRO",
apiversion="", apiversion="",
remoteaccessenabled=False, remoteaccessenabled=True,
remoteaccess="abc321.blah.blah", remoteaccess="abc321.blah.blah",
firmwareversion="555", firmwareversion="555",
pin=123, pin=123,

View File

@ -340,6 +340,7 @@ async def test_device_info_ismartgate(
assert device.name == "mycontroller" assert device.name == "mycontroller"
assert device.model == "ismartgatePRO" assert device.model == "ismartgatePRO"
assert device.sw_version == "555" assert device.sw_version == "555"
assert device.configuration_url == "https://abc321.blah.blah"
@patch("homeassistant.components.gogogate2.common.GogoGate2Api") @patch("homeassistant.components.gogogate2.common.GogoGate2Api")
@ -375,3 +376,4 @@ async def test_device_info_gogogate2(
assert device.name == "mycontroller" assert device.name == "mycontroller"
assert device.model == "gogogate2" assert device.model == "gogogate2"
assert device.sw_version == "222" assert device.sw_version == "222"
assert device.configuration_url == "http://127.0.0.1"

View File

@ -48,13 +48,13 @@ FAN_ACTION = "fan_action"
PRESET_HOLD = "Hold" PRESET_HOLD = "Hold"
async def test_no_thermostats( async def test_no_thermostat_options(
hass: HomeAssistant, device: MagicMock, config_entry: MagicMock hass: HomeAssistant, device: MagicMock, config_entry: MagicMock
) -> None: ) -> None:
"""Test the setup of the climate entities when there are no appliances available.""" """Test the setup of the climate entities when there are no additional options available."""
device._data = {} device._data = {}
await init_integration(hass, config_entry) await init_integration(hass, config_entry)
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 1
async def test_static_attributes( async def test_static_attributes(

View File

@ -163,7 +163,6 @@ async def test_number_validator() -> None:
CONF_COUNT: 2, CONF_COUNT: 2,
CONF_DATA_TYPE: DataType.CUSTOM, CONF_DATA_TYPE: DataType.CUSTOM,
CONF_STRUCTURE: ">i", CONF_STRUCTURE: ">i",
CONF_SWAP: CONF_SWAP_BYTE,
}, },
], ],
) )
@ -221,6 +220,22 @@ async def test_ok_struct_validator(do_config) -> None:
CONF_STRUCTURE: ">f", CONF_STRUCTURE: ">f",
CONF_SLAVE_COUNT: 5, CONF_SLAVE_COUNT: 5,
}, },
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_DATA_TYPE: DataType.STRING,
CONF_SLAVE_COUNT: 2,
},
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_DATA_TYPE: DataType.INT16,
CONF_SWAP: CONF_SWAP_WORD,
},
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_COUNT: 2,
CONF_SLAVE_COUNT: 2,
CONF_DATA_TYPE: DataType.INT32,
},
], ],
) )
async def test_exception_struct_validator(do_config) -> None: async def test_exception_struct_validator(do_config) -> None:

View File

@ -243,7 +243,7 @@ async def test_config_sensor(hass: HomeAssistant, mock_modbus) -> None:
}, },
] ]
}, },
f"Error in sensor {TEST_ENTITY_NAME} swap(word) not possible due to the registers count: 1, needed: 2", f"{TEST_ENTITY_NAME}: `structure` illegal with `swap`",
), ),
], ],
) )
@ -603,9 +603,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
CONF_ADDRESS: 51, CONF_ADDRESS: 51,
CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_DATA_TYPE: DataType.UINT32, CONF_DATA_TYPE: DataType.UINT32,
CONF_SCALE: 1, CONF_SCAN_INTERVAL: 1,
CONF_OFFSET: 0,
CONF_PRECISION: 0,
}, },
], ],
}, },
@ -677,17 +675,184 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
) )
async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
"""Run test for sensor.""" """Run test for sensor."""
assert hass.states.get(ENTITY_ID).state == expected[0]
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
for i in range(0, len(expected)):
for i in range(1, len(expected)): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_")
entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i}".replace(" ", "_") unique_id = f"{SLAVE_UNIQUE_ID}"
assert hass.states.get(entity_id).state == expected[i] if i:
unique_id = f"{SLAVE_UNIQUE_ID}_{i}" entity_id = f"{entity_id}_{i}"
unique_id = f"{unique_id}_{i}"
entry = entity_registry.async_get(entity_id) entry = entity_registry.async_get(entity_id)
state = hass.states.get(entity_id).state
assert state == expected[i]
assert entry.unique_id == unique_id assert entry.unique_id == unique_id
@pytest.mark.parametrize(
"do_config",
[
{
CONF_SENSORS: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 51,
CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_SCAN_INTERVAL: 1,
},
],
},
],
)
@pytest.mark.parametrize(
("config_addon", "register_words", "do_exception", "expected"),
[
(
{
CONF_SLAVE_COUNT: 0,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_SWAP: CONF_SWAP_BYTE,
CONF_DATA_TYPE: DataType.UINT16,
},
[0x0102],
False,
[str(int(0x0201))],
),
(
{
CONF_SLAVE_COUNT: 0,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_SWAP: CONF_SWAP_WORD,
CONF_DATA_TYPE: DataType.UINT32,
},
[0x0102, 0x0304],
False,
[str(int(0x03040102))],
),
(
{
CONF_SLAVE_COUNT: 0,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_SWAP: CONF_SWAP_WORD,
CONF_DATA_TYPE: DataType.UINT64,
},
[0x0102, 0x0304, 0x0506, 0x0708],
False,
[str(int(0x0708050603040102))],
),
(
{
CONF_SLAVE_COUNT: 1,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_DATA_TYPE: DataType.UINT16,
CONF_SWAP: CONF_SWAP_BYTE,
},
[0x0102, 0x0304],
False,
[str(int(0x0201)), str(int(0x0403))],
),
(
{
CONF_SLAVE_COUNT: 1,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_DATA_TYPE: DataType.UINT32,
CONF_SWAP: CONF_SWAP_WORD,
},
[0x0102, 0x0304, 0x0506, 0x0708],
False,
[str(int(0x03040102)), str(int(0x07080506))],
),
(
{
CONF_SLAVE_COUNT: 1,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_DATA_TYPE: DataType.UINT64,
CONF_SWAP: CONF_SWAP_WORD,
},
[0x0102, 0x0304, 0x0506, 0x0708, 0x0901, 0x0902, 0x0903, 0x0904],
False,
[str(int(0x0708050603040102)), str(int(0x0904090309020901))],
),
(
{
CONF_SLAVE_COUNT: 3,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_DATA_TYPE: DataType.UINT16,
CONF_SWAP: CONF_SWAP_BYTE,
},
[0x0102, 0x0304, 0x0506, 0x0708],
False,
[str(int(0x0201)), str(int(0x0403)), str(int(0x0605)), str(int(0x0807))],
),
(
{
CONF_SLAVE_COUNT: 3,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_DATA_TYPE: DataType.UINT32,
CONF_SWAP: CONF_SWAP_WORD,
},
[
0x0102,
0x0304,
0x0506,
0x0708,
0x090A,
0x0B0C,
0x0D0E,
0x0F00,
],
False,
[
str(int(0x03040102)),
str(int(0x07080506)),
str(int(0x0B0C090A)),
str(int(0x0F000D0E)),
],
),
(
{
CONF_SLAVE_COUNT: 3,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_DATA_TYPE: DataType.UINT64,
CONF_SWAP: CONF_SWAP_WORD,
},
[
0x0601,
0x0602,
0x0603,
0x0604,
0x0701,
0x0702,
0x0703,
0x0704,
0x0801,
0x0802,
0x0803,
0x0804,
0x0901,
0x0902,
0x0903,
0x0904,
],
False,
[
str(int(0x0604060306020601)),
str(int(0x0704070307020701)),
str(int(0x0804080308020801)),
str(int(0x0904090309020901)),
],
),
],
)
async def test_slave_swap_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
"""Run test for sensor."""
for i in range(0, len(expected)):
entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_")
if i:
entity_id = f"{entity_id}_{i}"
state = hass.states.get(entity_id).state
assert state == expected[i]
@pytest.mark.parametrize( @pytest.mark.parametrize(
"do_config", "do_config",
[ [

View File

@ -1,7 +1,7 @@
"""Tests for the ness_alarm component.""" """Tests for the ness_alarm component."""
from enum import Enum
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from nessclient import ArmingMode, ArmingState
import pytest import pytest
from homeassistant.components import alarm_control_panel from homeassistant.components import alarm_control_panel
@ -24,6 +24,8 @@ from homeassistant.const import (
SERVICE_ALARM_DISARM, SERVICE_ALARM_DISARM,
SERVICE_ALARM_TRIGGER, SERVICE_ALARM_TRIGGER,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMING, STATE_ALARM_ARMING,
STATE_ALARM_DISARMED, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_PENDING,
@ -84,7 +86,7 @@ async def test_dispatch_state_change(hass: HomeAssistant, mock_nessclient) -> No
await hass.async_block_till_done() await hass.async_block_till_done()
on_state_change = mock_nessclient.on_state_change.call_args[0][0] on_state_change = mock_nessclient.on_state_change.call_args[0][0]
on_state_change(MockArmingState.ARMING) on_state_change(ArmingState.ARMING, None)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.is_state("alarm_control_panel.alarm_panel", STATE_ALARM_ARMING) assert hass.states.is_state("alarm_control_panel.alarm_panel", STATE_ALARM_ARMING)
@ -174,13 +176,16 @@ async def test_dispatch_zone_change(hass: HomeAssistant, mock_nessclient) -> Non
async def test_arming_state_change(hass: HomeAssistant, mock_nessclient) -> None: async def test_arming_state_change(hass: HomeAssistant, mock_nessclient) -> None:
"""Test arming state change handing.""" """Test arming state change handing."""
states = [ states = [
(MockArmingState.UNKNOWN, STATE_UNKNOWN), (ArmingState.UNKNOWN, None, STATE_UNKNOWN),
(MockArmingState.DISARMED, STATE_ALARM_DISARMED), (ArmingState.DISARMED, None, STATE_ALARM_DISARMED),
(MockArmingState.ARMING, STATE_ALARM_ARMING), (ArmingState.ARMING, None, STATE_ALARM_ARMING),
(MockArmingState.EXIT_DELAY, STATE_ALARM_ARMING), (ArmingState.EXIT_DELAY, None, STATE_ALARM_ARMING),
(MockArmingState.ARMED, STATE_ALARM_ARMED_AWAY), (ArmingState.ARMED, None, STATE_ALARM_ARMED_AWAY),
(MockArmingState.ENTRY_DELAY, STATE_ALARM_PENDING), (ArmingState.ARMED, ArmingMode.ARMED_AWAY, STATE_ALARM_ARMED_AWAY),
(MockArmingState.TRIGGERED, STATE_ALARM_TRIGGERED), (ArmingState.ARMED, ArmingMode.ARMED_HOME, STATE_ALARM_ARMED_HOME),
(ArmingState.ARMED, ArmingMode.ARMED_NIGHT, STATE_ALARM_ARMED_NIGHT),
(ArmingState.ENTRY_DELAY, None, STATE_ALARM_PENDING),
(ArmingState.TRIGGERED, None, STATE_ALARM_TRIGGERED),
] ]
await async_setup_component(hass, DOMAIN, VALID_CONFIG) await async_setup_component(hass, DOMAIN, VALID_CONFIG)
@ -188,24 +193,12 @@ async def test_arming_state_change(hass: HomeAssistant, mock_nessclient) -> None
assert hass.states.is_state("alarm_control_panel.alarm_panel", STATE_UNKNOWN) assert hass.states.is_state("alarm_control_panel.alarm_panel", STATE_UNKNOWN)
on_state_change = mock_nessclient.on_state_change.call_args[0][0] on_state_change = mock_nessclient.on_state_change.call_args[0][0]
for arming_state, expected_state in states: for arming_state, arming_mode, expected_state in states:
on_state_change(arming_state) on_state_change(arming_state, arming_mode)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.is_state("alarm_control_panel.alarm_panel", expected_state) assert hass.states.is_state("alarm_control_panel.alarm_panel", expected_state)
class MockArmingState(Enum):
"""Mock nessclient.ArmingState enum."""
UNKNOWN = "UNKNOWN"
DISARMED = "DISARMED"
ARMING = "ARMING"
EXIT_DELAY = "EXIT_DELAY"
ARMED = "ARMED"
ENTRY_DELAY = "ENTRY_DELAY"
TRIGGERED = "TRIGGERED"
class MockClient: class MockClient:
"""Mock nessclient.Client stub.""" """Mock nessclient.Client stub."""
@ -253,10 +246,5 @@ def mock_nessclient():
with patch( with patch(
"homeassistant.components.ness_alarm.Client", new=_mock_factory, create=True "homeassistant.components.ness_alarm.Client", new=_mock_factory, create=True
), patch(
"homeassistant.components.ness_alarm.ArmingState", new=MockArmingState
), patch(
"homeassistant.components.ness_alarm.alarm_control_panel.ArmingState",
new=MockArmingState,
): ):
yield _mock_instance yield _mock_instance

View File

@ -68,6 +68,21 @@ async def test_setup_platform(hass: HomeAssistant) -> None:
assert hass.services.has_service(notify.DOMAIN, "tts_test") assert hass.services.has_service(notify.DOMAIN, "tts_test")
async def test_setup_platform_missing_key(hass: HomeAssistant) -> None:
"""Test platform without required tts_service or entity_id key."""
config = {
notify.DOMAIN: {
"platform": "tts",
"name": "tts_test",
"media_player": "media_player.demo",
}
}
with assert_setup_component(0, notify.DOMAIN):
assert await async_setup_component(hass, notify.DOMAIN, config)
assert not hass.services.has_service(notify.DOMAIN, "tts_test")
async def test_setup_legacy_service(hass: HomeAssistant) -> None: async def test_setup_legacy_service(hass: HomeAssistant) -> None:
"""Set up the demo platform and call service.""" """Set up the demo platform and call service."""
calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)