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
STABLE_BLE_VERSION_STR = "2023.6.0"
STABLE_BLE_VERSION_STR = "2023.8.0"
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
PROJECT_URLS = {
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",

View File

@ -54,5 +54,5 @@
"iot_class": "local_push",
"loggers": ["flux_led"],
"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",
"integration_type": "system",
"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:
"""Device info for the controller."""
data = self.coordinator.data
configuration_url = (
f"https://{data.remoteaccess}" if data.remoteaccess else None
)
if data.remoteaccessenabled:
configuration_url = f"https://{data.remoteaccess}"
else:
configuration_url = f"http://{self._config_entry.data[CONF_IP_ADDRESS]}"
return DeviceInfo(
configuration_url=configuration_url,
identifiers={(DOMAIN, str(self._config_entry.unique_id))},

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

View File

@ -146,13 +146,13 @@ class HoneywellUSThermostat(ClimateEntity):
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
if device._data["canControlHumidification"]:
if device._data.get("canControlHumidification"):
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
if not device._data["hasFan"]:
if not device._data.get("hasFan"):
return
# 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.typing import StateType
from . import HoneywellData
from .const import DOMAIN, HUMIDITY_STATUS_KEY, TEMPERATURE_STATUS_KEY
@ -71,7 +72,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Honeywell thermostat."""
data = hass.data[DOMAIN][config_entry.entry_id]
data: HoneywellData = hass.data[DOMAIN][config_entry.entry_id]
sensors = []
for device in data.devices.values():

View File

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

View File

@ -21,7 +21,12 @@ from homeassistant.components.climate import (
HVACMode,
)
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.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
@ -113,7 +118,6 @@ async def async_setup_entry(
),
location,
device,
hass.config.units.temperature_unit,
)
)
@ -140,10 +144,15 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
description: ClimateEntityDescription,
location: LyricLocation,
device: LyricDevice,
temperature_unit: str,
) -> None:
"""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
self._attr_hvac_modes = [HVACMode.OFF]
@ -176,11 +185,6 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
return SUPPORT_FLAGS_LCC
return SUPPORT_FLAGS_TCC
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return self._temperature_unit
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""

View File

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

View File

@ -48,10 +48,12 @@ from .const import (
CONF_MIN_VALUE,
CONF_PRECISION,
CONF_SCALE,
CONF_SLAVE_COUNT,
CONF_STATE_OFF,
CONF_STATE_ON,
CONF_SWAP,
CONF_SWAP_BYTE,
CONF_SWAP_NONE,
CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE,
CONF_VERIFY,
@ -155,15 +157,25 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
"""Initialize the switch."""
super().__init__(hub, config)
self._swap = config[CONF_SWAP]
if self._swap == CONF_SWAP_NONE:
self._swap = None
self._data_type = config[CONF_DATA_TYPE]
self._structure: str = config[CONF_STRUCTURE]
self._precision = config[CONF_PRECISION]
self._scale = config[CONF_SCALE]
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."""
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):
# convert [12][34] --> [21][43]
for i, register in enumerate(registers):
@ -191,7 +203,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
def unpack_structure_result(self, registers: list[int]) -> str | None:
"""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])
if self._data_type == DataType.STRING:
return byte_string.decode()

View File

@ -206,7 +206,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
int.from_bytes(as_bytes[i : i + 2], "big")
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 (
DataType.INT16,

View File

@ -68,7 +68,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
"""Initialize the modbus register sensor."""
super().__init__(hub, entry)
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._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT)
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)
else:
self._attr_native_value = result
if self._attr_native_value is None:
self._attr_available = False
else:
self._attr_available = True
self._attr_available = self._attr_native_value is not None
self._lazy_errors = self._lazy_error_count
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]
structure = config.get(CONF_STRUCTURE)
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 structure:
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 CONF_COUNT not in config:
config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count
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}"
else:
if slave_count > 1:
error = f"{name} structure: cannot be mixed with {CONF_SLAVE_COUNT}"
if config[CONF_DATA_TYPE] == DataType.CUSTOM:
if slave or slave_count > 1:
error = f"{name}: `{CONF_STRUCTURE}` illegal with `{CONF_SLAVE_COUNT}` / `{CONF_SLAVE}`"
raise vol.Invalid(error)
if swap_type != CONF_SWAP_NONE:
error = f"{name}: `{CONF_STRUCTURE}` illegal with `{CONF_SWAP}`"
raise vol.Invalid(error)
if not structure:
error = (
@ -102,19 +102,37 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]:
f"Structure request {size} 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 CONF_COUNT not in config:
config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count
if swap_type != CONF_SWAP_NONE:
if swap_type == CONF_SWAP_BYTE:
regs_needed = 1
else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD
regs_needed = 2
count = config[CONF_COUNT]
if count < regs_needed or (count % regs_needed) != 0:
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 {
**config,
CONF_STRUCTURE: structure,

View File

@ -3,7 +3,7 @@ from collections import namedtuple
import datetime
import logging
from nessclient import ArmingState, Client
from nessclient import ArmingMode, ArmingState, Client
import voluptuous as vol
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)
)
def on_state_change(arming_state: ArmingState):
def on_state_change(arming_state: ArmingState, arming_mode: ArmingMode | None):
"""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_state_change(on_state_change)

View File

@ -3,12 +3,15 @@ from __future__ import annotations
import logging
from nessclient import ArmingState, Client
from nessclient import ArmingMode, ArmingState, Client
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_VACATION,
STATE_ALARM_ARMING,
STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
@ -23,6 +26,15 @@ from . import DATA_NESS, SIGNAL_ARMING_STATE_CHANGED
_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(
hass: HomeAssistant,
@ -79,7 +91,9 @@ class NessAlarmPanel(alarm.AlarmControlPanelEntity):
await self._client.panic(code)
@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."""
if arming_state == ArmingState.UNKNOWN:
@ -91,7 +105,9 @@ class NessAlarmPanel(alarm.AlarmControlPanelEntity):
elif arming_state == ArmingState.EXIT_DELAY:
self._attr_state = STATE_ALARM_ARMING
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:
self._attr_state = STATE_ALARM_PENDING
elif arming_state == ArmingState.TRIGGERED:

View File

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

View File

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

View File

@ -6,5 +6,5 @@
"dependencies": ["recorder"],
"documentation": "https://www.home-assistant.io/integrations/opower",
"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",
"iot_class": "local_polling",
"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 reolink_aio.api import (
DUAL_LENS_DUAL_MOTION_MODELS,
FACE_DETECTION_TYPE,
PERSON_DETECTION_TYPE,
PET_DETECTION_TYPE,
@ -128,6 +129,9 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt
super().__init__(reolink_data, channel)
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 = (
f"{self._host.unique_id}_{self._channel}_{entity_description.key}"
)

View File

@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"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",
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": ["python-roborock==0.32.2"]
"requirements": ["python-roborock==0.32.3"]
}

View File

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

View File

@ -533,7 +533,8 @@ RPC_SENSORS: Final = {
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
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,
),
"total_current": RpcSensorDescription(

View File

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

View File

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

View File

@ -20,16 +20,19 @@ ENTITY_LEGACY_PROVIDER_GROUP = "entity_or_legacy_provider"
_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),
PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_NAME): cv.string,
vol.Exclusive(CONF_TTS_SERVICE, ENTITY_LEGACY_PROVIDER_GROUP): cv.entity_id,
vol.Exclusive(CONF_ENTITY_ID, ENTITY_LEGACY_PROVIDER_GROUP): cv.entities_domain(
DOMAIN
),
vol.Exclusive(
CONF_ENTITY_ID, ENTITY_LEGACY_PROVIDER_GROUP
): cv.entities_domain(DOMAIN),
vol.Required(CONF_MEDIA_PLAYER): cv.entity_id,
vol.Optional(ATTR_LANGUAGE): cv.string,
}
),
)

View File

@ -83,7 +83,8 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
raise UpdateFailed("Could not read overview") from err
def unpack(overview: list, value: str) -> dict | list:
return next(
return (
next(
(
item["data"]["installation"][value]
for item in overview
@ -91,6 +92,8 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
),
[],
)
or []
)
# Store data in a way Home Assistant can easily consume it
self._overview = overview

View File

@ -438,22 +438,16 @@ class DataManager:
async def async_get_sleep_summary(self) -> dict[Measurement, Any]:
"""Get the sleep summary data."""
_LOGGER.debug("Updating withing sleep summary")
now = dt_util.utcnow()
now = dt_util.now()
yesterday = now - datetime.timedelta(days=1)
yesterday_noon = datetime.datetime(
yesterday.year,
yesterday.month,
yesterday.day,
12,
0,
0,
0,
datetime.UTC,
yesterday_noon = dt_util.start_of_local_day(yesterday) + datetime.timedelta(
hours=12
)
yesterday_noon_utc = dt_util.as_utc(yesterday_noon)
def get_sleep_summary() -> SleepGetSummaryResponse:
return self._api.sleep_get_summary(
lastupdate=yesterday_noon,
lastupdate=yesterday_noon_utc,
data_fields=[
GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY,
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):
"""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):
try:
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
@ -403,8 +403,8 @@ def _async_setup_services(hass: HomeAssistant):
)
class YeelightGenericLight(YeelightEntity, LightEntity):
"""Representation of a Yeelight generic light."""
class YeelightBaseLight(YeelightEntity, LightEntity):
"""Abstract Yeelight light."""
_attr_color_mode = 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)
class YeelightColorLightSupport(YeelightGenericLight):
class YeelightGenericLight(YeelightBaseLight):
"""Representation of a generic Yeelight."""
_attr_name = None
class YeelightColorLightSupport(YeelightBaseLight):
"""Representation of a Color Yeelight light support."""
_attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS, ColorMode.RGB}
@ -884,7 +890,7 @@ class YeelightColorLightSupport(YeelightGenericLight):
return YEELIGHT_COLOR_EFFECT_LIST
class YeelightWhiteTempLightSupport(YeelightGenericLight):
class YeelightWhiteTempLightSupport(YeelightBaseLight):
"""Representation of a White temp Yeelight light."""
_attr_name = None
@ -904,7 +910,7 @@ class YeelightNightLightSupport:
return PowerMode.NORMAL
class YeelightWithoutNightlightSwitchMixIn(YeelightGenericLight):
class YeelightWithoutNightlightSwitchMixIn(YeelightBaseLight):
"""A mix-in for yeelights without a nightlight switch."""
@property
@ -940,7 +946,7 @@ class YeelightColorLightWithoutNightlightSwitchLight(
class YeelightColorLightWithNightlightSwitch(
YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight
YeelightNightLightSupport, YeelightColorLightSupport, YeelightBaseLight
):
"""Representation of a Yeelight with rgb support and nightlight.
@ -964,7 +970,7 @@ class YeelightWhiteTempWithoutNightlightSwitch(
class YeelightWithNightLight(
YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightGenericLight
YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightBaseLight
):
"""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
class YeelightNightLightMode(YeelightGenericLight):
class YeelightNightLightMode(YeelightBaseLight):
"""Representation of a Yeelight when in nightlight mode."""
_attr_color_mode = ColorMode.BRIGHTNESS

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

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

View File

@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
requires = ["setuptools==68.0.0", "wheel~=0.40.0"]
build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.8.2"
version = "2023.8.3"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

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

View File

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

View File

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

View File

@ -340,6 +340,7 @@ async def test_device_info_ismartgate(
assert device.name == "mycontroller"
assert device.model == "ismartgatePRO"
assert device.sw_version == "555"
assert device.configuration_url == "https://abc321.blah.blah"
@patch("homeassistant.components.gogogate2.common.GogoGate2Api")
@ -375,3 +376,4 @@ async def test_device_info_gogogate2(
assert device.name == "mycontroller"
assert device.model == "gogogate2"
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"
async def test_no_thermostats(
async def test_no_thermostat_options(
hass: HomeAssistant, device: MagicMock, config_entry: MagicMock
) -> 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 = {}
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(

View File

@ -163,7 +163,6 @@ async def test_number_validator() -> None:
CONF_COUNT: 2,
CONF_DATA_TYPE: DataType.CUSTOM,
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_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:

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_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_DATA_TYPE: DataType.UINT32,
CONF_SCALE: 1,
CONF_OFFSET: 0,
CONF_PRECISION: 0,
CONF_SCAN_INTERVAL: 1,
},
],
},
@ -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:
"""Run test for sensor."""
assert hass.states.get(ENTITY_ID).state == expected[0]
entity_registry = er.async_get(hass)
for i in range(1, len(expected)):
entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i}".replace(" ", "_")
assert hass.states.get(entity_id).state == expected[i]
unique_id = f"{SLAVE_UNIQUE_ID}_{i}"
for i in range(0, len(expected)):
entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_")
unique_id = f"{SLAVE_UNIQUE_ID}"
if i:
entity_id = f"{entity_id}_{i}"
unique_id = f"{unique_id}_{i}"
entry = entity_registry.async_get(entity_id)
state = hass.states.get(entity_id).state
assert state == expected[i]
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(
"do_config",
[

View File

@ -1,7 +1,7 @@
"""Tests for the ness_alarm component."""
from enum import Enum
from unittest.mock import MagicMock, patch
from nessclient import ArmingMode, ArmingState
import pytest
from homeassistant.components import alarm_control_panel
@ -24,6 +24,8 @@ from homeassistant.const import (
SERVICE_ALARM_DISARM,
SERVICE_ALARM_TRIGGER,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMING,
STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
@ -84,7 +86,7 @@ async def test_dispatch_state_change(hass: HomeAssistant, mock_nessclient) -> No
await hass.async_block_till_done()
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()
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:
"""Test arming state change handing."""
states = [
(MockArmingState.UNKNOWN, STATE_UNKNOWN),
(MockArmingState.DISARMED, STATE_ALARM_DISARMED),
(MockArmingState.ARMING, STATE_ALARM_ARMING),
(MockArmingState.EXIT_DELAY, STATE_ALARM_ARMING),
(MockArmingState.ARMED, STATE_ALARM_ARMED_AWAY),
(MockArmingState.ENTRY_DELAY, STATE_ALARM_PENDING),
(MockArmingState.TRIGGERED, STATE_ALARM_TRIGGERED),
(ArmingState.UNKNOWN, None, STATE_UNKNOWN),
(ArmingState.DISARMED, None, STATE_ALARM_DISARMED),
(ArmingState.ARMING, None, STATE_ALARM_ARMING),
(ArmingState.EXIT_DELAY, None, STATE_ALARM_ARMING),
(ArmingState.ARMED, None, STATE_ALARM_ARMED_AWAY),
(ArmingState.ARMED, ArmingMode.ARMED_AWAY, STATE_ALARM_ARMED_AWAY),
(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)
@ -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)
on_state_change = mock_nessclient.on_state_change.call_args[0][0]
for arming_state, expected_state in states:
on_state_change(arming_state)
for arming_state, arming_mode, expected_state in states:
on_state_change(arming_state, arming_mode)
await hass.async_block_till_done()
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:
"""Mock nessclient.Client stub."""
@ -253,10 +246,5 @@ def mock_nessclient():
with patch(
"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

View File

@ -68,6 +68,21 @@ async def test_setup_platform(hass: HomeAssistant) -> None:
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:
"""Set up the demo platform and call service."""
calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)