This commit is contained in:
Paulus Schoutsen 2022-08-25 15:51:17 -04:00 committed by GitHub
commit f2e177a5af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 332 additions and 73 deletions

View File

@ -90,6 +90,7 @@ class AladdinDevice(CoverEntity):
self._number = device["door_number"] self._number = device["door_number"]
self._name = device["name"] self._name = device["name"]
self._serial = device["serial"] self._serial = device["serial"]
self._model = device["model"]
self._attr_unique_id = f"{self._device_id}-{self._number}" self._attr_unique_id = f"{self._device_id}-{self._number}"
self._attr_has_entity_name = True self._attr_has_entity_name = True
@ -97,9 +98,10 @@ class AladdinDevice(CoverEntity):
def device_info(self) -> DeviceInfo | None: def device_info(self) -> DeviceInfo | None:
"""Device information for Aladdin Connect cover.""" """Device information for Aladdin Connect cover."""
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self._device_id)}, identifiers={(DOMAIN, f"{self._device_id}-{self._number}")},
name=self._name, name=self._name,
manufacturer="Overhead Door", manufacturer="Overhead Door",
model=self._model,
) )
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:

View File

@ -12,3 +12,4 @@ class DoorDevice(TypedDict):
name: str name: str
status: str status: str
serial: str serial: str
model: str

View File

@ -56,6 +56,15 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=AladdinConnectClient.get_rssi_status, value_fn=AladdinConnectClient.get_rssi_status,
), ),
AccSensorEntityDescription(
key="ble_strength",
name="BLE Strength",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_registry_enabled_default=False,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=AladdinConnectClient.get_ble_strength,
),
) )
@ -89,22 +98,26 @@ class AladdinConnectSensor(SensorEntity):
device: DoorDevice, device: DoorDevice,
description: AccSensorEntityDescription, description: AccSensorEntityDescription,
) -> None: ) -> None:
"""Initialize a sensor for an Abode device.""" """Initialize a sensor for an Aladdin Connect device."""
self._device_id = device["device_id"] self._device_id = device["device_id"]
self._number = device["door_number"] self._number = device["door_number"]
self._name = device["name"] self._name = device["name"]
self._model = device["model"]
self._acc = acc self._acc = acc
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{self._device_id}-{self._number}-{description.key}" self._attr_unique_id = f"{self._device_id}-{self._number}-{description.key}"
self._attr_has_entity_name = True self._attr_has_entity_name = True
if self._model == "01" and description.key in ("battery_level", "ble_strength"):
self._attr_entity_registry_enabled_default = True
@property @property
def device_info(self) -> DeviceInfo | None: def device_info(self) -> DeviceInfo | None:
"""Device information for Aladdin Connect sensors.""" """Device information for Aladdin Connect sensors."""
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self._device_id)}, identifiers={(DOMAIN, f"{self._device_id}-{self._number}")},
name=self._name, name=self._name,
manufacturer="Overhead Door", manufacturer="Overhead Door",
model=self._model,
) )
@property @property

View File

@ -48,7 +48,7 @@ def get_alarm_system_id_for_unique_id(
gateway: DeconzGateway, unique_id: str gateway: DeconzGateway, unique_id: str
) -> str | None: ) -> str | None:
"""Retrieve alarm system ID the unique ID is registered to.""" """Retrieve alarm system ID the unique ID is registered to."""
for alarm_system in gateway.api.alarmsystems.values(): for alarm_system in gateway.api.alarm_systems.values():
if unique_id in alarm_system.devices: if unique_id in alarm_system.devices:
return alarm_system.resource_id return alarm_system.resource_id
return None return None
@ -123,27 +123,27 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity):
async def async_alarm_arm_away(self, code: str | None = None) -> None: async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command.""" """Send arm away command."""
if code: if code:
await self.gateway.api.alarmsystems.arm( await self.gateway.api.alarm_systems.arm(
self.alarm_system_id, AlarmSystemArmAction.AWAY, code self.alarm_system_id, AlarmSystemArmAction.AWAY, code
) )
async def async_alarm_arm_home(self, code: str | None = None) -> None: async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command.""" """Send arm home command."""
if code: if code:
await self.gateway.api.alarmsystems.arm( await self.gateway.api.alarm_systems.arm(
self.alarm_system_id, AlarmSystemArmAction.STAY, code self.alarm_system_id, AlarmSystemArmAction.STAY, code
) )
async def async_alarm_arm_night(self, code: str | None = None) -> None: async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command.""" """Send arm night command."""
if code: if code:
await self.gateway.api.alarmsystems.arm( await self.gateway.api.alarm_systems.arm(
self.alarm_system_id, AlarmSystemArmAction.NIGHT, code self.alarm_system_id, AlarmSystemArmAction.NIGHT, code
) )
async def async_alarm_disarm(self, code: str | None = None) -> None: async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""
if code: if code:
await self.gateway.api.alarmsystems.arm( await self.gateway.api.alarm_systems.arm(
self.alarm_system_id, AlarmSystemArmAction.DISARM, code self.alarm_system_id, AlarmSystemArmAction.DISARM, code
) )

View File

@ -159,7 +159,7 @@ ENTITY_DESCRIPTIONS = {
], ],
} }
BINARY_SENSOR_DESCRIPTIONS = [ COMMON_BINARY_SENSOR_DESCRIPTIONS = [
DeconzBinarySensorDescription( DeconzBinarySensorDescription(
key="tampered", key="tampered",
value_fn=lambda device: device.tampered, value_fn=lambda device: device.tampered,
@ -215,7 +215,8 @@ async def async_setup_entry(
sensor = gateway.api.sensors[sensor_id] sensor = gateway.api.sensors[sensor_id]
for description in ( for description in (
ENTITY_DESCRIPTIONS.get(type(sensor), []) + BINARY_SENSOR_DESCRIPTIONS ENTITY_DESCRIPTIONS.get(type(sensor), [])
+ COMMON_BINARY_SENSOR_DESCRIPTIONS
): ):
if ( if (
not hasattr(sensor, description.key) not hasattr(sensor, description.key)
@ -284,8 +285,8 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity):
if self._device.on is not None: if self._device.on is not None:
attr[ATTR_ON] = self._device.on attr[ATTR_ON] = self._device.on
if self._device.secondary_temperature is not None: if self._device.internal_temperature is not None:
attr[ATTR_TEMPERATURE] = self._device.secondary_temperature attr[ATTR_TEMPERATURE] = self._device.internal_temperature
if isinstance(self._device, Presence): if isinstance(self._device, Presence):

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any, cast from typing import Any, cast
from pydeconz.interfaces.lights import CoverAction from pydeconz.interfaces.lights import CoverAction
from pydeconz.models import ResourceType
from pydeconz.models.event import EventType from pydeconz.models.event import EventType
from pydeconz.models.light.cover import Cover from pydeconz.models.light.cover import Cover
@ -23,9 +24,9 @@ from .deconz_device import DeconzDevice
from .gateway import DeconzGateway, get_gateway_from_config_entry from .gateway import DeconzGateway, get_gateway_from_config_entry
DECONZ_TYPE_TO_DEVICE_CLASS = { DECONZ_TYPE_TO_DEVICE_CLASS = {
"Level controllable output": CoverDeviceClass.DAMPER, ResourceType.LEVEL_CONTROLLABLE_OUTPUT.value: CoverDeviceClass.DAMPER,
"Window covering controller": CoverDeviceClass.SHADE, ResourceType.WINDOW_COVERING_CONTROLLER.value: CoverDeviceClass.SHADE,
"Window covering device": CoverDeviceClass.SHADE, ResourceType.WINDOW_COVERING_DEVICE.value: CoverDeviceClass.SHADE,
} }
@ -72,6 +73,8 @@ class DeconzCover(DeconzDevice, CoverEntity):
self._attr_device_class = DECONZ_TYPE_TO_DEVICE_CLASS.get(cover.type) self._attr_device_class = DECONZ_TYPE_TO_DEVICE_CLASS.get(cover.type)
self.legacy_mode = cover.type == ResourceType.LEVEL_CONTROLLABLE_OUTPUT.value
@property @property
def current_cover_position(self) -> int: def current_cover_position(self) -> int:
"""Return the current position of the cover.""" """Return the current position of the cover."""
@ -88,6 +91,7 @@ class DeconzCover(DeconzDevice, CoverEntity):
await self.gateway.api.lights.covers.set_state( await self.gateway.api.lights.covers.set_state(
id=self._device.resource_id, id=self._device.resource_id,
lift=position, lift=position,
legacy_mode=self.legacy_mode,
) )
async def async_open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
@ -95,6 +99,7 @@ class DeconzCover(DeconzDevice, CoverEntity):
await self.gateway.api.lights.covers.set_state( await self.gateway.api.lights.covers.set_state(
id=self._device.resource_id, id=self._device.resource_id,
action=CoverAction.OPEN, action=CoverAction.OPEN,
legacy_mode=self.legacy_mode,
) )
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
@ -102,6 +107,7 @@ class DeconzCover(DeconzDevice, CoverEntity):
await self.gateway.api.lights.covers.set_state( await self.gateway.api.lights.covers.set_state(
id=self._device.resource_id, id=self._device.resource_id,
action=CoverAction.CLOSE, action=CoverAction.CLOSE,
legacy_mode=self.legacy_mode,
) )
async def async_stop_cover(self, **kwargs: Any) -> None: async def async_stop_cover(self, **kwargs: Any) -> None:
@ -109,6 +115,7 @@ class DeconzCover(DeconzDevice, CoverEntity):
await self.gateway.api.lights.covers.set_state( await self.gateway.api.lights.covers.set_state(
id=self._device.resource_id, id=self._device.resource_id,
action=CoverAction.STOP, action=CoverAction.STOP,
legacy_mode=self.legacy_mode,
) )
@property @property
@ -124,6 +131,7 @@ class DeconzCover(DeconzDevice, CoverEntity):
await self.gateway.api.lights.covers.set_state( await self.gateway.api.lights.covers.set_state(
id=self._device.resource_id, id=self._device.resource_id,
tilt=position, tilt=position,
legacy_mode=self.legacy_mode,
) )
async def async_open_cover_tilt(self, **kwargs: Any) -> None: async def async_open_cover_tilt(self, **kwargs: Any) -> None:
@ -131,6 +139,7 @@ class DeconzCover(DeconzDevice, CoverEntity):
await self.gateway.api.lights.covers.set_state( await self.gateway.api.lights.covers.set_state(
id=self._device.resource_id, id=self._device.resource_id,
tilt=0, tilt=0,
legacy_mode=self.legacy_mode,
) )
async def async_close_cover_tilt(self, **kwargs: Any) -> None: async def async_close_cover_tilt(self, **kwargs: Any) -> None:
@ -138,6 +147,7 @@ class DeconzCover(DeconzDevice, CoverEntity):
await self.gateway.api.lights.covers.set_state( await self.gateway.api.lights.covers.set_state(
id=self._device.resource_id, id=self._device.resource_id,
tilt=100, tilt=100,
legacy_mode=self.legacy_mode,
) )
async def async_stop_cover_tilt(self, **kwargs: Any) -> None: async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
@ -145,4 +155,5 @@ class DeconzCover(DeconzDevice, CoverEntity):
await self.gateway.api.lights.covers.set_state( await self.gateway.api.lights.covers.set_state(
id=self._device.resource_id, id=self._device.resource_id,
action=CoverAction.STOP, action=CoverAction.STOP,
legacy_mode=self.legacy_mode,
) )

View File

@ -26,7 +26,7 @@ async def async_get_config_entry_diagnostics(
gateway.api.config.raw, REDACT_DECONZ_CONFIG gateway.api.config.raw, REDACT_DECONZ_CONFIG
) )
diag["websocket_state"] = ( diag["websocket_state"] = (
gateway.api.websocket.state if gateway.api.websocket else "Unknown" gateway.api.websocket.state.value if gateway.api.websocket else "Unknown"
) )
diag["deconz_ids"] = gateway.deconz_ids diag["deconz_ids"] = gateway.deconz_ids
diag["entities"] = gateway.entities diag["entities"] = gateway.entities
@ -37,7 +37,7 @@ async def async_get_config_entry_diagnostics(
} }
for event in gateway.events for event in gateway.events
} }
diag["alarm_systems"] = {k: v.raw for k, v in gateway.api.alarmsystems.items()} diag["alarm_systems"] = {k: v.raw for k, v in gateway.api.alarm_systems.items()}
diag["groups"] = {k: v.raw for k, v in gateway.api.groups.items()} diag["groups"] = {k: v.raw for k, v in gateway.api.groups.items()}
diag["lights"] = {k: v.raw for k, v in gateway.api.lights.items()} diag["lights"] = {k: v.raw for k, v in gateway.api.lights.items()}
diag["scenes"] = {k: v.raw for k, v in gateway.api.scenes.items()} diag["scenes"] = {k: v.raw for k, v in gateway.api.scenes.items()}

View File

@ -169,7 +169,7 @@ class DeconzGateway:
) )
) )
for device_id in deconz_device_interface: for device_id in sorted(deconz_device_interface, key=int):
async_add_device(EventType.ADDED, device_id) async_add_device(EventType.ADDED, device_id)
initializing = False initializing = False

View File

@ -3,7 +3,7 @@
"name": "deCONZ", "name": "deCONZ",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz", "documentation": "https://www.home-assistant.io/integrations/deconz",
"requirements": ["pydeconz==102"], "requirements": ["pydeconz==104"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Royal Philips Electronics", "manufacturer": "Royal Philips Electronics",

View File

@ -6,7 +6,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from pydeconz.models.event import EventType from pydeconz.models.event import EventType
from pydeconz.models.sensor.presence import PRESENCE_DELAY, Presence from pydeconz.models.sensor.presence import Presence
from homeassistant.components.number import ( from homeassistant.components.number import (
DOMAIN, DOMAIN,
@ -42,7 +42,7 @@ ENTITY_DESCRIPTIONS = {
key="delay", key="delay",
value_fn=lambda device: device.delay, value_fn=lambda device: device.delay,
suffix="Delay", suffix="Delay",
update_key=PRESENCE_DELAY, update_key="delay",
native_max_value=65535, native_max_value=65535,
native_min_value=0, native_min_value=0,
native_step=1, native_step=1,

View File

@ -209,7 +209,7 @@ ENTITY_DESCRIPTIONS = {
} }
SENSOR_DESCRIPTIONS = [ COMMON_SENSOR_DESCRIPTIONS = [
DeconzSensorDescription( DeconzSensorDescription(
key="battery", key="battery",
value_fn=lambda device: device.battery, value_fn=lambda device: device.battery,
@ -221,8 +221,8 @@ SENSOR_DESCRIPTIONS = [
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
DeconzSensorDescription( DeconzSensorDescription(
key="secondary_temperature", key="internal_temperature",
value_fn=lambda device: device.secondary_temperature, value_fn=lambda device: device.internal_temperature,
suffix="Temperature", suffix="Temperature",
update_key="temperature", update_key="temperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
@ -253,7 +253,7 @@ async def async_setup_entry(
known_entities = set(gateway.entities[DOMAIN]) known_entities = set(gateway.entities[DOMAIN])
for description in ( for description in (
ENTITY_DESCRIPTIONS.get(type(sensor), []) + SENSOR_DESCRIPTIONS ENTITY_DESCRIPTIONS.get(type(sensor), []) + COMMON_SENSOR_DESCRIPTIONS
): ):
if ( if (
not hasattr(sensor, description.key) not hasattr(sensor, description.key)
@ -342,8 +342,8 @@ class DeconzSensor(DeconzDevice, SensorEntity):
if self._device.on is not None: if self._device.on is not None:
attr[ATTR_ON] = self._device.on attr[ATTR_ON] = self._device.on
if self._device.secondary_temperature is not None: if self._device.internal_temperature is not None:
attr[ATTR_TEMPERATURE] = self._device.secondary_temperature attr[ATTR_TEMPERATURE] = self._device.internal_temperature
if isinstance(self._device, Consumption): if isinstance(self._device, Consumption):
attr[ATTR_POWER] = self._device.power attr[ATTR_POWER] = self._device.power
@ -384,14 +384,16 @@ class DeconzBatteryTracker:
self.sensor = gateway.api.sensors[sensor_id] self.sensor = gateway.api.sensors[sensor_id]
self.gateway = gateway self.gateway = gateway
self.async_add_entities = async_add_entities self.async_add_entities = async_add_entities
self.unsub = self.sensor.subscribe(self.async_update_callback) self.unsubscribe = self.sensor.subscribe(self.async_update_callback)
@callback @callback
def async_update_callback(self) -> None: def async_update_callback(self) -> None:
"""Update the device's state.""" """Update the device's state."""
if "battery" in self.sensor.changed_keys: if "battery" in self.sensor.changed_keys:
self.unsub() self.unsubscribe()
known_entities = set(self.gateway.entities[DOMAIN]) known_entities = set(self.gateway.entities[DOMAIN])
entity = DeconzSensor(self.sensor, self.gateway, SENSOR_DESCRIPTIONS[0]) entity = DeconzSensor(
self.sensor, self.gateway, COMMON_SENSOR_DESCRIPTIONS[0]
)
if entity.unique_id not in known_entities: if entity.unique_id not in known_entities:
self.async_add_entities([entity]) self.async_add_entities([entity])

View File

@ -22,6 +22,7 @@ from homeassistant.const import (
ELECTRIC_POTENTIAL_VOLT, ELECTRIC_POTENTIAL_VOLT,
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
ENERGY_WATT_HOUR, ENERGY_WATT_HOUR,
FREQUENCY_HERTZ,
POWER_WATT, POWER_WATT,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -252,6 +253,7 @@ SENSOR_UNIT_MAPPING = {
"A": ELECTRIC_CURRENT_AMPERE, "A": ELECTRIC_CURRENT_AMPERE,
"V": ELECTRIC_POTENTIAL_VOLT, "V": ELECTRIC_POTENTIAL_VOLT,
"°": DEGREE, "°": DEGREE,
"Hz": FREQUENCY_HERTZ,
} }

View File

@ -25,6 +25,7 @@ class GoodweNumberEntityDescriptionBase:
getter: Callable[[Inverter], Awaitable[int]] getter: Callable[[Inverter], Awaitable[int]]
setter: Callable[[Inverter, int], Awaitable[None]] setter: Callable[[Inverter, int], Awaitable[None]]
filter: Callable[[Inverter], bool]
@dataclass @dataclass
@ -35,17 +36,33 @@ class GoodweNumberEntityDescription(
NUMBERS = ( NUMBERS = (
# non DT inverters (limit in W)
GoodweNumberEntityDescription( GoodweNumberEntityDescription(
key="grid_export_limit", key="grid_export_limit",
name="Grid export limit", name="Grid export limit",
icon="mdi:transmission-tower", icon="mdi:transmission-tower",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=POWER_WATT, native_unit_of_measurement=POWER_WATT,
getter=lambda inv: inv.get_grid_export_limit(),
setter=lambda inv, val: inv.set_grid_export_limit(val),
native_step=100, native_step=100,
native_min_value=0, native_min_value=0,
native_max_value=10000, native_max_value=10000,
getter=lambda inv: inv.get_grid_export_limit(),
setter=lambda inv, val: inv.set_grid_export_limit(val),
filter=lambda inv: type(inv).__name__ != "DT",
),
# DT inverters (limit is in %)
GoodweNumberEntityDescription(
key="grid_export_limit",
name="Grid export limit",
icon="mdi:transmission-tower",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
native_step=1,
native_min_value=0,
native_max_value=100,
getter=lambda inv: inv.get_grid_export_limit(),
setter=lambda inv, val: inv.set_grid_export_limit(val),
filter=lambda inv: type(inv).__name__ == "DT",
), ),
GoodweNumberEntityDescription( GoodweNumberEntityDescription(
key="battery_discharge_depth", key="battery_discharge_depth",
@ -53,11 +70,12 @@ NUMBERS = (
icon="mdi:battery-arrow-down", icon="mdi:battery-arrow-down",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
getter=lambda inv: inv.get_ongrid_battery_dod(),
setter=lambda inv, val: inv.set_ongrid_battery_dod(val),
native_step=1, native_step=1,
native_min_value=0, native_min_value=0,
native_max_value=99, native_max_value=99,
getter=lambda inv: inv.get_ongrid_battery_dod(),
setter=lambda inv, val: inv.set_ongrid_battery_dod(val),
filter=lambda inv: True,
), ),
) )
@ -73,7 +91,7 @@ async def async_setup_entry(
entities = [] entities = []
for description in NUMBERS: for description in filter(lambda dsc: dsc.filter(inverter), NUMBERS):
try: try:
current_value = await description.getter(inverter) current_value = await description.getter(inverter)
except (InverterError, ValueError): except (InverterError, ValueError):
@ -82,7 +100,7 @@ async def async_setup_entry(
continue continue
entities.append( entities.append(
InverterNumberEntity(device_info, description, inverter, current_value), InverterNumberEntity(device_info, description, inverter, current_value)
) )
async_add_entities(entities) async_add_entities(entities)

View File

@ -8,11 +8,15 @@ DEFAULT_PLANT_ID = "0"
DEFAULT_NAME = "Growatt" DEFAULT_NAME = "Growatt"
SERVER_URLS = [ SERVER_URLS = [
"https://server.growatt.com/", "https://server-api.growatt.com/",
"https://server-us.growatt.com/", "https://server-us.growatt.com/",
"http://server.smten.com/", "http://server.smten.com/",
] ]
DEPRECATED_URLS = [
"https://server.growatt.com/",
]
DEFAULT_URL = SERVER_URLS[0] DEFAULT_URL = SERVER_URLS[0]
DOMAIN = "growatt_server" DOMAIN = "growatt_server"

View File

@ -19,6 +19,7 @@ from .const import (
CONF_PLANT_ID, CONF_PLANT_ID,
DEFAULT_PLANT_ID, DEFAULT_PLANT_ID,
DEFAULT_URL, DEFAULT_URL,
DEPRECATED_URLS,
DOMAIN, DOMAIN,
LOGIN_INVALID_AUTH_CODE, LOGIN_INVALID_AUTH_CODE,
) )
@ -62,12 +63,23 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Growatt sensor.""" """Set up the Growatt sensor."""
config = config_entry.data config = {**config_entry.data}
username = config[CONF_USERNAME] username = config[CONF_USERNAME]
password = config[CONF_PASSWORD] password = config[CONF_PASSWORD]
url = config.get(CONF_URL, DEFAULT_URL) url = config.get(CONF_URL, DEFAULT_URL)
name = config[CONF_NAME] name = config[CONF_NAME]
# If the URL has been deprecated then change to the default instead
if url in DEPRECATED_URLS:
_LOGGER.info(
"URL: %s has been deprecated, migrating to the latest default: %s",
url,
DEFAULT_URL,
)
url = DEFAULT_URL
config[CONF_URL] = url
hass.config_entries.async_update_entry(config_entry, data=config)
api = growattServer.GrowattApi() api = growattServer.GrowattApi()
api.server_url = url api.server_url = url

View File

@ -3,7 +3,7 @@
"name": "KNX", "name": "KNX",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx", "documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": ["xknx==1.0.0"], "requirements": ["xknx==1.0.1"],
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
"quality_scale": "platinum", "quality_scale": "platinum",
"iot_class": "local_push", "iot_class": "local_push",

View File

@ -13,7 +13,7 @@ from homeassistant.const import (
CONF_NAME, CONF_NAME,
LENGTH_MILLIMETERS, LENGTH_MILLIMETERS,
PRESSURE_HPA, PRESSURE_HPA,
SPEED_METERS_PER_SECOND, SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -58,7 +58,7 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
_attr_native_precipitation_unit = LENGTH_MILLIMETERS _attr_native_precipitation_unit = LENGTH_MILLIMETERS
_attr_native_pressure_unit = PRESSURE_HPA _attr_native_pressure_unit = PRESSURE_HPA
_attr_native_temperature_unit = TEMP_CELSIUS _attr_native_temperature_unit = TEMP_CELSIUS
_attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR
def __init__(self, coordinator, config, hourly): def __init__(self, coordinator, config, hourly):
"""Initialise the platform with a data instance and site.""" """Initialise the platform with a data instance and site."""

View File

@ -188,7 +188,10 @@ class BlockSleepingClimate(
def hvac_mode(self) -> HVACMode: def hvac_mode(self) -> HVACMode:
"""HVAC current mode.""" """HVAC current mode."""
if self.device_block is None: if self.device_block is None:
return HVACMode(self.last_state.state) if self.last_state else HVACMode.OFF if self.last_state and self.last_state.state in list(HVACMode):
return HVACMode(self.last_state.state)
return HVACMode.OFF
if self.device_block.mode is None or self._check_is_off(): if self.device_block.mode is None or self._check_is_off():
return HVACMode.OFF return HVACMode.OFF

View File

@ -2,7 +2,7 @@
"domain": "switchbot", "domain": "switchbot",
"name": "SwitchBot", "name": "SwitchBot",
"documentation": "https://www.home-assistant.io/integrations/switchbot", "documentation": "https://www.home-assistant.io/integrations/switchbot",
"requirements": ["PySwitchbot==0.18.10"], "requirements": ["PySwitchbot==0.18.14"],
"config_flow": true, "config_flow": true,
"dependencies": ["bluetooth"], "dependencies": ["bluetooth"],
"codeowners": [ "codeowners": [

View File

@ -4,15 +4,15 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha", "documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [ "requirements": [
"bellows==0.32.0", "bellows==0.33.1",
"pyserial==3.5", "pyserial==3.5",
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.78", "zha-quirks==0.0.78",
"zigpy-deconz==0.18.0", "zigpy-deconz==0.18.0",
"zigpy==0.49.1", "zigpy==0.50.2",
"zigpy-xbee==0.15.0", "zigpy-xbee==0.15.0",
"zigpy-zigate==0.9.1", "zigpy-zigate==0.9.2",
"zigpy-znp==0.8.1" "zigpy-znp==0.8.2"
], ],
"usb": [ "usb": [
{ {

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 8 MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "6" PATCH_VERSION: Final = "7"
__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, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2022.8.6" version = "2022.8.7"
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

@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1 PySocks==1.7.1
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.18.10 PySwitchbot==0.18.14
# homeassistant.components.transport_nsw # homeassistant.components.transport_nsw
PyTransportNSW==0.1.1 PyTransportNSW==0.1.1
@ -396,7 +396,7 @@ beautifulsoup4==4.11.1
# beewi_smartclim==0.0.10 # beewi_smartclim==0.0.10
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.32.0 bellows==0.33.1
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.2 bimmer_connected==0.10.2
@ -1455,7 +1455,7 @@ pydaikin==2.7.0
pydanfossair==0.1.0 pydanfossair==0.1.0
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==102 pydeconz==104
# homeassistant.components.delijn # homeassistant.components.delijn
pydelijn==1.0.0 pydelijn==1.0.0
@ -2473,7 +2473,7 @@ xboxapi==2.0.1
xiaomi-ble==0.6.4 xiaomi-ble==0.6.4
# homeassistant.components.knx # homeassistant.components.knx
xknx==1.0.0 xknx==1.0.1
# homeassistant.components.bluesound # homeassistant.components.bluesound
# homeassistant.components.fritz # homeassistant.components.fritz
@ -2529,13 +2529,13 @@ zigpy-deconz==0.18.0
zigpy-xbee==0.15.0 zigpy-xbee==0.15.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-zigate==0.9.1 zigpy-zigate==0.9.2
# homeassistant.components.zha # homeassistant.components.zha
zigpy-znp==0.8.1 zigpy-znp==0.8.2
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.49.1 zigpy==0.50.2
# homeassistant.components.zoneminder # homeassistant.components.zoneminder
zm-py==0.5.2 zm-py==0.5.2

View File

@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1 PySocks==1.7.1
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.18.10 PySwitchbot==0.18.14
# homeassistant.components.transport_nsw # homeassistant.components.transport_nsw
PyTransportNSW==0.1.1 PyTransportNSW==0.1.1
@ -320,7 +320,7 @@ base36==0.1.1
beautifulsoup4==4.11.1 beautifulsoup4==4.11.1
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.32.0 bellows==0.33.1
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.2 bimmer_connected==0.10.2
@ -1001,7 +1001,7 @@ pycoolmasternet-async==0.1.2
pydaikin==2.7.0 pydaikin==2.7.0
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==102 pydeconz==104
# homeassistant.components.dexcom # homeassistant.components.dexcom
pydexcom==0.2.3 pydexcom==0.2.3
@ -1665,7 +1665,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.6.4 xiaomi-ble==0.6.4
# homeassistant.components.knx # homeassistant.components.knx
xknx==1.0.0 xknx==1.0.1
# homeassistant.components.bluesound # homeassistant.components.bluesound
# homeassistant.components.fritz # homeassistant.components.fritz
@ -1703,13 +1703,13 @@ zigpy-deconz==0.18.0
zigpy-xbee==0.15.0 zigpy-xbee==0.15.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-zigate==0.9.1 zigpy-zigate==0.9.2
# homeassistant.components.zha # homeassistant.components.zha
zigpy-znp==0.8.1 zigpy-znp==0.8.2
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.49.1 zigpy==0.50.2
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.39.0 zwave-js-server-python==0.39.0

View File

@ -11,6 +11,7 @@ DEVICE_CONFIG_OPEN = {
"status": "open", "status": "open",
"link_status": "Connected", "link_status": "Connected",
"serial": "12345", "serial": "12345",
"model": "02",
} }
@ -31,6 +32,8 @@ def fixture_mock_aladdinconnect_api():
mock_opener.get_battery_status.return_value = "99" mock_opener.get_battery_status.return_value = "99"
mock_opener.async_get_rssi_status = AsyncMock(return_value="-55") mock_opener.async_get_rssi_status = AsyncMock(return_value="-55")
mock_opener.get_rssi_status.return_value = "-55" mock_opener.get_rssi_status.return_value = "-55"
mock_opener.async_get_ble_strength = AsyncMock(return_value="-45")
mock_opener.get_ble_strength.return_value = "-45"
mock_opener.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) mock_opener.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN])
mock_opener.register_callback = mock.Mock(return_value=True) mock_opener.register_callback = mock.Mock(return_value=True)

View File

@ -1,6 +1,6 @@
"""Test the Aladdin Connect Sensors.""" """Test the Aladdin Connect Sensors."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from homeassistant.components.aladdin_connect.const import DOMAIN from homeassistant.components.aladdin_connect.const import DOMAIN
from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL
@ -10,6 +10,17 @@ from homeassistant.util.dt import utcnow
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
DEVICE_CONFIG_MODEL_01 = {
"device_id": 533255,
"door_number": 1,
"name": "home",
"status": "closed",
"link_status": "Connected",
"serial": "12345",
"model": "01",
}
CONFIG = {"username": "test-user", "password": "test-password"} CONFIG = {"username": "test-user", "password": "test-password"}
RELOAD_AFTER_UPDATE_DELAY = timedelta(seconds=31) RELOAD_AFTER_UPDATE_DELAY = timedelta(seconds=31)
@ -83,3 +94,71 @@ async def test_sensors(
state = hass.states.get("sensor.home_wi_fi_rssi") state = hass.states.get("sensor.home_wi_fi_rssi")
assert state assert state
async def test_sensors_model_01(
hass: HomeAssistant,
mock_aladdinconnect_api: MagicMock,
) -> None:
"""Test Sensors for AladdinConnect."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=CONFIG,
unique_id="test-id",
)
config_entry.add_to_hass(hass)
await hass.async_block_till_done()
with patch(
"homeassistant.components.aladdin_connect.AladdinConnectClient",
return_value=mock_aladdinconnect_api,
):
mock_aladdinconnect_api.get_doors = AsyncMock(
return_value=[DEVICE_CONFIG_MODEL_01]
)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
registry = entity_registry.async_get(hass)
entry = registry.async_get("sensor.home_battery_level")
assert entry
assert entry.disabled is False
assert entry.disabled_by is None
state = hass.states.get("sensor.home_battery_level")
assert state
entry = registry.async_get("sensor.home_wi_fi_rssi")
await hass.async_block_till_done()
assert entry
assert entry.disabled
assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION
update_entry = registry.async_update_entity(
entry.entity_id, **{"disabled_by": None}
)
await hass.async_block_till_done()
assert update_entry != entry
assert update_entry.disabled is False
state = hass.states.get("sensor.home_wi_fi_rssi")
assert state is None
update_entry = registry.async_update_entity(
entry.entity_id, **{"disabled_by": None}
)
await hass.async_block_till_done()
async_fire_time_changed(
hass,
utcnow() + SCAN_INTERVAL,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.home_wi_fi_rssi")
assert state
entry = registry.async_get("sensor.home_ble_strength")
await hass.async_block_till_done()
assert entry
assert entry.disabled is False
assert entry.disabled_by is None
state = hass.states.get("sensor.home_ble_strength")
assert state

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from unittest.mock import patch from unittest.mock import patch
from pydeconz.websocket import SIGNAL_CONNECTION_STATE, SIGNAL_DATA from pydeconz.websocket import Signal
import pytest import pytest
from tests.components.light.conftest import mock_light_profiles # noqa: F401 from tests.components.light.conftest import mock_light_profiles # noqa: F401
@ -20,10 +20,10 @@ def mock_deconz_websocket():
if data: if data:
mock.return_value.data = data mock.return_value.data = data
await pydeconz_gateway_session_handler(signal=SIGNAL_DATA) await pydeconz_gateway_session_handler(signal=Signal.DATA)
elif state: elif state:
mock.return_value.state = state mock.return_value.state = state
await pydeconz_gateway_session_handler(signal=SIGNAL_CONNECTION_STATE) await pydeconz_gateway_session_handler(signal=Signal.CONNECTION_STATE)
else: else:
raise NotImplementedError raise NotImplementedError

View File

@ -213,3 +213,111 @@ async def test_tilt_cover(hass, aioclient_mock):
blocking=True, blocking=True,
) )
assert aioclient_mock.mock_calls[4][2] == {"stop": True} assert aioclient_mock.mock_calls[4][2] == {"stop": True}
async def test_level_controllable_output_cover(hass, aioclient_mock):
"""Test that tilting a cover works."""
data = {
"lights": {
"0": {
"etag": "4cefc909134c8e99086b55273c2bde67",
"hascolor": False,
"lastannounced": "2022-08-08T12:06:18Z",
"lastseen": "2022-08-14T14:22Z",
"manufacturername": "Keen Home Inc",
"modelid": "SV01-410-MP-1.0",
"name": "Vent",
"state": {
"alert": "none",
"bri": 242,
"on": False,
"reachable": True,
"sat": 10,
},
"swversion": "0x00000012",
"type": "Level controllable output",
"uniqueid": "00:22:a3:00:00:00:00:00-01",
}
}
}
with patch.dict(DECONZ_WEB_REQUEST, data):
config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 1
covering_device = hass.states.get("cover.vent")
assert covering_device.state == STATE_OPEN
assert covering_device.attributes[ATTR_CURRENT_TILT_POSITION] == 97
# Verify service calls for tilting cover
mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state")
# Service open cover
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: "cover.vent"},
blocking=True,
)
assert aioclient_mock.mock_calls[1][2] == {"on": False}
# Service close cover
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: "cover.vent"},
blocking=True,
)
assert aioclient_mock.mock_calls[2][2] == {"on": True}
# Service set cover position
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_SET_COVER_POSITION,
{ATTR_ENTITY_ID: "cover.vent", ATTR_POSITION: 40},
blocking=True,
)
assert aioclient_mock.mock_calls[3][2] == {"bri": 152}
# Service set tilt cover
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_SET_COVER_TILT_POSITION,
{ATTR_ENTITY_ID: "cover.vent", ATTR_TILT_POSITION: 40},
blocking=True,
)
assert aioclient_mock.mock_calls[4][2] == {"sat": 152}
# Service open tilt cover
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER_TILT,
{ATTR_ENTITY_ID: "cover.vent"},
blocking=True,
)
assert aioclient_mock.mock_calls[5][2] == {"sat": 0}
# Service close tilt cover
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER_TILT,
{ATTR_ENTITY_ID: "cover.vent"},
blocking=True,
)
assert aioclient_mock.mock_calls[6][2] == {"sat": 254}
# Service stop cover movement
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_STOP_COVER_TILT,
{ATTR_ENTITY_ID: "cover.vent"},
blocking=True,
)
assert aioclient_mock.mock_calls[7][2] == {"bri_inc": 0}

View File

@ -1,6 +1,6 @@
"""Test deCONZ diagnostics.""" """Test deCONZ diagnostics."""
from pydeconz.websocket import STATE_RUNNING from pydeconz.websocket import State
from homeassistant.components.deconz.const import CONF_MASTER_GATEWAY from homeassistant.components.deconz.const import CONF_MASTER_GATEWAY
from homeassistant.components.diagnostics import REDACTED from homeassistant.components.diagnostics import REDACTED
@ -17,7 +17,7 @@ async def test_entry_diagnostics(
"""Test config entry diagnostics.""" """Test config entry diagnostics."""
config_entry = await setup_deconz_integration(hass, aioclient_mock) config_entry = await setup_deconz_integration(hass, aioclient_mock)
await mock_deconz_websocket(state=STATE_RUNNING) await mock_deconz_websocket(state=State.RUNNING)
await hass.async_block_till_done() await hass.async_block_till_done()
assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {
@ -44,7 +44,7 @@ async def test_entry_diagnostics(
"uuid": "1234", "uuid": "1234",
"websocketport": 1234, "websocketport": 1234,
}, },
"websocket_state": STATE_RUNNING, "websocket_state": State.RUNNING.value,
"deconz_ids": {}, "deconz_ids": {},
"entities": { "entities": {
str(Platform.ALARM_CONTROL_PANEL): [], str(Platform.ALARM_CONTROL_PANEL): [],

View File

@ -5,7 +5,7 @@ from copy import deepcopy
from unittest.mock import patch from unittest.mock import patch
import pydeconz import pydeconz
from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING from pydeconz.websocket import State
import pytest import pytest
from homeassistant.components import ssdp from homeassistant.components import ssdp
@ -223,12 +223,12 @@ async def test_connection_status_signalling(
assert hass.states.get("binary_sensor.presence").state == STATE_OFF assert hass.states.get("binary_sensor.presence").state == STATE_OFF
await mock_deconz_websocket(state=STATE_RETRYING) await mock_deconz_websocket(state=State.RETRYING)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("binary_sensor.presence").state == STATE_UNAVAILABLE assert hass.states.get("binary_sensor.presence").state == STATE_UNAVAILABLE
await mock_deconz_websocket(state=STATE_RUNNING) await mock_deconz_websocket(state=State.RUNNING)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("binary_sensor.presence").state == STATE_OFF assert hass.states.get("binary_sensor.presence").state == STATE_OFF