Compare commits

..

5 Commits

Author SHA1 Message Date
G Johansson
02c0cfd680 Don't load from platform yaml 2024-07-12 17:20:04 +00:00
G Johansson
22bb68d610 Add tests 2024-07-12 17:15:18 +00:00
G Johansson
42fe1d6097 Fixes 2024-07-12 17:14:55 +00:00
G Johansson
ebb450db48 Fixes 2024-07-12 15:05:09 +00:00
G Johansson
d73e12df93 Add config flow to compensation helper 2024-07-12 14:35:49 +00:00
371 changed files with 18021 additions and 21111 deletions

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.25.12
uses: github/codeql-action/init@v3.25.11
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.25.12
uses: github/codeql-action/analyze@v3.25.11
with:
category: "/language:python"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.2
rev: v0.5.1
hooks:
- id: ruff
args:

View File

@@ -21,7 +21,6 @@ homeassistant.helpers.entity_platform
homeassistant.helpers.entity_values
homeassistant.helpers.event
homeassistant.helpers.reload
homeassistant.helpers.script
homeassistant.helpers.script_variables
homeassistant.helpers.singleton
homeassistant.helpers.sun
@@ -385,7 +384,6 @@ homeassistant.components.samsungtv.*
homeassistant.components.scene.*
homeassistant.components.schedule.*
homeassistant.components.scrape.*
homeassistant.components.script.*
homeassistant.components.search.*
homeassistant.components.select.*
homeassistant.components.sensibo.*

View File

@@ -743,8 +743,8 @@ build.json @home-assistant/supervisor
/tests/components/kitchen_sink/ @home-assistant/core
/homeassistant/components/kmtronic/ @dgomes
/tests/components/kmtronic/ @dgomes
/homeassistant/components/knocki/ @joostlek @jgatto1 @JakeBosh
/tests/components/knocki/ @joostlek @jgatto1 @JakeBosh
/homeassistant/components/knocki/ @joostlek @jgatto1
/tests/components/knocki/ @joostlek @jgatto1
/homeassistant/components/knx/ @Julius2342 @farmio @marvin-w
/tests/components/knx/ @Julius2342 @farmio @marvin-w
/homeassistant/components/kodi/ @OnFreund
@@ -884,6 +884,8 @@ build.json @home-assistant/supervisor
/tests/components/moat/ @bdraco
/homeassistant/components/mobile_app/ @home-assistant/core
/tests/components/mobile_app/ @home-assistant/core
/homeassistant/components/modbus/ @janiversen
/tests/components/modbus/ @janiversen
/homeassistant/components/modem_callerid/ @tkdrob
/tests/components/modem_callerid/ @tkdrob
/homeassistant/components/modern_forms/ @wonderslug

View File

@@ -82,54 +82,33 @@ async def async_setup_entry(
"""Add Airzone binary sensors from a config_entry."""
coordinator = entry.runtime_data
added_systems: set[str] = set()
added_zones: set[str] = set()
binary_sensors: list[AirzoneBinarySensor] = [
AirzoneSystemBinarySensor(
coordinator,
description,
entry,
system_id,
system_data,
)
for system_id, system_data in coordinator.data[AZD_SYSTEMS].items()
for description in SYSTEM_BINARY_SENSOR_TYPES
if description.key in system_data
]
def _async_entity_listener() -> None:
"""Handle additions of binary sensors."""
binary_sensors.extend(
AirzoneZoneBinarySensor(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
for description in ZONE_BINARY_SENSOR_TYPES
if description.key in zone_data
)
entities: list[AirzoneBinarySensor] = []
systems_data = coordinator.data.get(AZD_SYSTEMS, {})
received_systems = set(systems_data)
new_systems = received_systems - added_systems
if new_systems:
entities.extend(
AirzoneSystemBinarySensor(
coordinator,
description,
entry,
system_id,
systems_data.get(system_id),
)
for system_id in new_systems
for description in SYSTEM_BINARY_SENSOR_TYPES
if description.key in systems_data.get(system_id)
)
added_systems.update(new_systems)
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
entities.extend(
AirzoneZoneBinarySensor(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_BINARY_SENSOR_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)
async_add_entities(entities)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
async_add_entities(binary_sensors)
class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):

View File

@@ -102,31 +102,17 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone climate from a config_entry."""
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of climate."""
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
async_add_entities(
AirzoneClimate(
coordinator,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
)
added_zones.update(new_zones)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
async_add_entities(
AirzoneClimate(
coordinator,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
)
class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):

View File

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

View File

@@ -83,34 +83,21 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone select from a config_entry."""
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of select."""
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
async_add_entities(
AirzoneZoneSelect(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
async_add_entities(
AirzoneZoneSelect(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for description in ZONE_SELECT_TYPES
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
if description.key in zone_data
)
class AirzoneBaseSelect(AirzoneEntity, SelectEntity):

View File

@@ -85,37 +85,21 @@ async def async_setup_entry(
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of sensors."""
entities: list[AirzoneSensor] = []
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
entities.extend(
AirzoneZoneSensor(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_SENSOR_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)
async_add_entities(entities)
entities: list[AirzoneSensor] = []
sensors: list[AirzoneSensor] = [
AirzoneZoneSensor(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
for description in ZONE_SENSOR_TYPES
if description.key in zone_data
]
if AZD_HOT_WATER in coordinator.data:
entities.extend(
sensors.extend(
AirzoneHotWaterSensor(
coordinator,
description,
@@ -126,7 +110,7 @@ async def async_setup_entry(
)
if AZD_WEBSERVER in coordinator.data:
entities.extend(
sensors.extend(
AirzoneWebServerSensor(
coordinator,
description,
@@ -136,10 +120,7 @@ async def async_setup_entry(
if description.key in coordinator.data[AZD_WEBSERVER]
)
async_add_entities(entities)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
async_add_entities(sensors)
class AirzoneSensor(AirzoneEntity, SensorEntity):

View File

@@ -61,7 +61,7 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone Water Heater from a config_entry."""
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
if AZD_HOT_WATER in coordinator.data:
async_add_entities([AirzoneWaterHeater(coordinator, entry)])

View File

@@ -1513,7 +1513,7 @@ async def async_api_adjust_range(
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
service = SERVICE_SET_COVER_POSITION
if not (current := entity.attributes.get(cover.ATTR_CURRENT_POSITION)):
if not (current := entity.attributes.get(cover.ATTR_POSITION)):
msg = f"Unable to determine {entity.entity_id} current position"
raise AlexaInvalidValueError(msg)
position = response_value = min(100, max(0, range_delta + current))

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/amazon_polly",
"iot_class": "cloud_push",
"loggers": ["boto3", "botocore", "s3transfer"],
"requirements": ["boto3==1.34.131"]
"requirements": ["boto3==1.34.51"]
}

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["python_homeassistant_analytics"],
"requirements": ["python-homeassistant-analytics==0.7.0"],
"requirements": ["python-homeassistant-analytics==0.6.0"],
"single_config_entry": true
}

View File

@@ -101,7 +101,7 @@
},
"learn_sendevent": {
"name": "Learn sendevent",
"description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of performing this action."
"description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service."
}
},
"exceptions": {

View File

@@ -8,5 +8,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["aioaquacell"],
"requirements": ["aioaquacell==0.2.0"]
"requirements": ["aioaquacell==0.1.8"]
}

View File

@@ -13,17 +13,17 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import (
DEFAULT_SCAN_INTERVAL,
DOMAIN,
DOMAIN_DATA_ENTRIES,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
SIGNAL_CLIENT_STOPPED,
)
type ArcamFmjConfigEntry = ConfigEntry[Client]
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@@ -31,21 +31,34 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
hass.data[DOMAIN_DATA_ENTRIES] = {}
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up config entry."""
entry.runtime_data = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
entries = hass.data[DOMAIN_DATA_ENTRIES]
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
entries[entry.entry_id] = client
entry.async_create_background_task(
hass, _run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL), "arcam_fmj"
hass, _run_client(hass, client, DEFAULT_SCAN_INTERVAL), "arcam_fmj"
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Cleanup before removing config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN_DATA_ENTRIES].pop(entry.entry_id)
return unload_ok
async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> None:

View File

@@ -10,11 +10,18 @@ from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn
import voluptuous as vol
from homeassistant.components import ssdp
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, DOMAIN_DATA_ENTRIES
def get_entry_client(hass: HomeAssistant, entry: ConfigEntry) -> Client:
"""Retrieve client associated with a config entry."""
client: Client = hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id]
return client
class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):

View File

@@ -11,3 +11,5 @@ EVENT_TURN_ON = "arcam_fmj.turn_on"
DEFAULT_PORT = 50000
DEFAULT_NAME = "Arcam FMJ"
DEFAULT_SCAN_INTERVAL = 5
DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries"

View File

@@ -19,6 +19,7 @@ from homeassistant.components.media_player import (
MediaType,
)
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -26,7 +27,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ArcamFmjConfigEntry
from .config_flow import get_entry_client
from .const import (
DOMAIN,
EVENT_TURN_ON,
@@ -40,12 +41,12 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ArcamFmjConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the configuration entry."""
client = config_entry.runtime_data
client = get_entry_client(hass, config_entry)
async_add_entities(
[

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==6.4.3", "yalexs-ble==2.4.3"]
"requirements": ["yalexs==6.4.2", "yalexs-ble==2.4.3"]
}

View File

@@ -9,3 +9,5 @@ from typing import Final
DOMAIN: Final = "autarco"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(minutes=5)
SENSORS_SOLAR: Final = "solar"

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import NamedTuple
from autarco import AccountSite, Autarco, Inverter, Solar
from autarco import AccountSite, Autarco, Solar
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -17,7 +17,6 @@ class AutarcoData(NamedTuple):
"""Class for defining data in dict."""
solar: Solar
inverters: dict[str, Inverter]
class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]):
@@ -45,5 +44,4 @@ class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]):
"""Fetch data from Autarco API."""
return AutarcoData(
solar=await self.client.get_solar(self.site.public_key),
inverters=await self.client.get_inverters(self.site.public_key),
)

View File

@@ -27,16 +27,6 @@ async def async_get_config_entry_diagnostics(
"energy_production_month": coordinator.data.solar.energy_production_month,
"energy_production_total": coordinator.data.solar.energy_production_total,
},
"inverters": [
{
"serial_number": inverter.serial_number,
"out_ac_power": inverter.out_ac_power,
"out_ac_energy_total": inverter.out_ac_energy_total,
"grid_turned_off": inverter.grid_turned_off,
"health": inverter.health,
}
for inverter in coordinator.data.inverters.values()
],
}
for coordinator in autarco_data
],

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from autarco import Inverter, Solar
from autarco import Solar
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -29,7 +29,7 @@ from .coordinator import AutarcoDataUpdateCoordinator
class AutarcoSolarSensorEntityDescription(SensorEntityDescription):
"""Describes an Autarco sensor entity."""
value_fn: Callable[[Solar], StateType]
state: Callable[[Solar], StateType]
SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = (
@@ -39,21 +39,21 @@ SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda solar: solar.power_production,
state=lambda solar: solar.power_production,
),
AutarcoSolarSensorEntityDescription(
key="energy_production_today",
translation_key="energy_production_today",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
value_fn=lambda solar: solar.energy_production_today,
state=lambda solar: solar.energy_production_today,
),
AutarcoSolarSensorEntityDescription(
key="energy_production_month",
translation_key="energy_production_month",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
value_fn=lambda solar: solar.energy_production_month,
state=lambda solar: solar.energy_production_month,
),
AutarcoSolarSensorEntityDescription(
key="energy_production_total",
@@ -61,34 +61,7 @@ SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda solar: solar.energy_production_total,
),
)
@dataclass(frozen=True, kw_only=True)
class AutarcoInverterSensorEntityDescription(SensorEntityDescription):
"""Describes an Autarco inverter sensor entity."""
value_fn: Callable[[Inverter], StateType]
SENSORS_INVERTER: tuple[AutarcoInverterSensorEntityDescription, ...] = (
AutarcoInverterSensorEntityDescription(
key="out_ac_power",
translation_key="out_ac_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda inverter: inverter.out_ac_power,
),
AutarcoInverterSensorEntityDescription(
key="out_ac_energy_total",
translation_key="out_ac_energy_total",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda inverter: inverter.out_ac_energy_total,
state=lambda solar: solar.energy_production_total,
),
)
@@ -99,25 +72,14 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Autarco sensors based on a config entry."""
entities: list[SensorEntity] = []
for coordinator in entry.runtime_data:
entities.extend(
async_add_entities(
AutarcoSolarSensorEntity(
coordinator=coordinator,
description=description,
)
for description in SENSORS_SOLAR
)
entities.extend(
AutarcoInverterSensorEntity(
coordinator=coordinator,
description=description,
serial_number=inverter,
)
for description in SENSORS_INVERTER
for inverter in coordinator.data.inverters
)
async_add_entities(entities)
class AutarcoSolarSensorEntity(
@@ -149,41 +111,4 @@ class AutarcoSolarSensorEntity(
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data.solar)
class AutarcoInverterSensorEntity(
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
):
"""Defines an Autarco inverter sensor."""
entity_description: AutarcoInverterSensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
*,
coordinator: AutarcoDataUpdateCoordinator,
description: AutarcoInverterSensorEntityDescription,
serial_number: str,
) -> None:
"""Initialize Autarco sensor."""
super().__init__(coordinator)
self.entity_description = description
self._serial_number = serial_number
self._attr_unique_id = f"{serial_number}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_number)},
name=f"Inverter {serial_number}",
manufacturer="Autarco",
model="Inverter",
serial_number=serial_number,
)
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(
self.coordinator.data.inverters[self._serial_number]
)
return self.entity_description.state(self.coordinator.data.solar)

View File

@@ -34,12 +34,6 @@
},
"energy_production_total": {
"name": "Energy production total"
},
"out_ac_power": {
"name": "Power AC output"
},
"out_ac_energy_total": {
"name": "Energy AC output total"
}
}
}

View File

@@ -333,7 +333,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Remove all automations and load new ones from config."""
await async_get_blueprints(hass).async_reset_cache()
conf = await component.async_prepare_reload(skip_reset=True)
if (conf := await component.async_prepare_reload(skip_reset=True)) is None:
return
if automation_id := service_call.data.get(CONF_ID):
await _async_process_single_config(hass, conf, component, automation_id)
else:

View File

@@ -37,12 +37,12 @@
},
"issues": {
"service_not_found": {
"title": "{name} uses an unknown action",
"title": "{name} uses an unknown service",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::automation::issues::service_not_found::title%]",
"description": "The automation \"{name}\" (`{entity_id}`) has an unknown action: `{service}`.\n\nThis error prevents the automation from running correctly. Maybe this action is no longer available, or perhaps a typo caused it.\n\nTo fix this error, [edit the automation]({edit}) and remove this action.\n\nClick on SUBMIT below to confirm you have fixed this automation."
"description": "The automation \"{name}\" (`{entity_id}`) has an action that calls an unknown service: `{service}`.\n\nThis error prevents the automation from running correctly. Maybe this service is no longer available, or perhaps a typo caused it.\n\nTo fix this error, [edit the automation]({edit}) and remove the action that calls this service.\n\nClick on SUBMIT below to confirm you have fixed this automation."
}
}
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/aws",
"iot_class": "cloud_push",
"loggers": ["aiobotocore", "botocore"],
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"]
"requirements": ["aiobotocore==2.13.0"]
}

View File

@@ -65,18 +65,13 @@ class AzureDataExplorerClient:
)
if data[CONF_USE_QUEUED_CLIENT] is True:
# Queued is the only option supported on free tier of ADX
# Queded is the only option supported on free tear of ADX
self.write_client = QueuedIngestClient(kcsb_ingest)
else:
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb_ingest)
self.query_client = KustoClient(kcsb_query)
# Reduce the HTTP logging, the default INFO logging is too verbose.
logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(
logging.WARNING
)
def test_connection(self) -> None:
"""Test connection, will throw Exception if it cannot connect."""
@@ -85,7 +80,7 @@ class AzureDataExplorerClient:
self.query_client.execute_query(self._database, query)
def ingest_data(self, adx_events: str) -> None:
"""Send data to Azure Data Explorer."""
"""Send data to Axure Data Explorer."""
bytes_stream = io.StringIO(adx_events)
stream_descriptor = StreamDescriptor(bytes_stream)

View File

@@ -3,7 +3,6 @@
"name": "Bayesian",
"codeowners": ["@HarvsG"],
"documentation": "https://www.home-assistant.io/integrations/bayesian",
"integration_type": "helper",
"iot_class": "local_polling",
"quality_scale": "internal"
}

View File

@@ -1,11 +1,10 @@
"""Support for Blinkstick lights."""
# mypy: ignore-errors
from __future__ import annotations
from typing import Any
# from blinkstick import blinkstick
from blinkstick import blinkstick
import voluptuous as vol
from homeassistant.components.light import (

View File

@@ -2,7 +2,6 @@
"domain": "blinksticklight",
"name": "BlinkStick",
"codeowners": [],
"disabled": "This integration is disabled because it uses non-open source code to operate.",
"documentation": "https://www.home-assistant.io/integrations/blinksticklight",
"iot_class": "local_polling",
"loggers": ["blinkstick"],

View File

@@ -1,5 +0,0 @@
extend = "../../../pyproject.toml"
lint.extend-ignore = [
"F821"
]

View File

@@ -131,7 +131,7 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
BMWSensorEntityDescription(
key="fuel_and_battery.remaining_fuel",
translation_key="remaining_fuel",
device_class=SensorDeviceClass.VOLUME_STORAGE,
device_class=SensorDeviceClass.VOLUME,
native_unit_of_measurement=UnitOfVolume.LITERS,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,

View File

@@ -49,7 +49,7 @@
"message": "Authentication failed for {email}, check your email and password"
},
"notify_missing_argument_item": {
"message": "Failed to perform action {service}. 'URGENT_MESSAGE' requires a value @ data['item']. Got None"
"message": "Failed to call service {service}. 'URGENT_MESSAGE' requires a value @ data['item']. Got None"
},
"notify_request_failed": {
"message": "Failed to send push notification for bring due to a connection error, try again later"

View File

@@ -8,7 +8,6 @@ DOMAINS_AND_TYPES = {
Platform.CLIMATE: {"HYS"},
Platform.LIGHT: {"LB1", "LB2"},
Platform.REMOTE: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"},
Platform.SELECT: {"HYS"},
Platform.SENSOR: {
"A1",
"MP1S",

View File

@@ -1,69 +0,0 @@
"""Support for Broadlink selects."""
from __future__ import annotations
from typing import Any
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BroadlinkDevice
from .const import DOMAIN
from .entity import BroadlinkEntity
DAY_ID_TO_NAME = {
1: "monday",
2: "tuesday",
3: "wednesday",
4: "thursday",
5: "friday",
6: "saturday",
7: "sunday",
}
DAY_NAME_TO_ID = {v: k for k, v in DAY_ID_TO_NAME.items()}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Broadlink select."""
device = hass.data[DOMAIN].devices[config_entry.entry_id]
async_add_entities([BroadlinkDayOfWeek(device)])
class BroadlinkDayOfWeek(BroadlinkEntity, SelectEntity):
"""Representation of a Broadlink day of week."""
_attr_has_entity_name = True
_attr_current_option: str | None = None
_attr_options = list(DAY_NAME_TO_ID)
_attr_translation_key = "day_of_week"
def __init__(self, device: BroadlinkDevice) -> None:
"""Initialize the select."""
super().__init__(device)
self._attr_unique_id = f"{device.unique_id}-dayofweek"
def _update_state(self, data: dict[str, Any]) -> None:
"""Update the state of the entity."""
if data is None or "dayofweek" not in data:
self._attr_current_option = None
else:
self._attr_current_option = DAY_ID_TO_NAME[data["dayofweek"]]
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self._device.async_request(
self._device.api.set_time,
hour=self._coordinator.data["hour"],
minute=self._coordinator.data["min"],
second=self._coordinator.data["sec"],
day=DAY_NAME_TO_ID[option],
)
self._attr_current_option = option
self.async_write_ha_state()

View File

@@ -61,20 +61,6 @@
"total_consumption": {
"name": "Total consumption"
}
},
"select": {
"day_of_week": {
"name": "Day of week",
"state": {
"monday": "[%key:common::time::monday%]",
"tuesday": "[%key:common::time::tuesday%]",
"wednesday": "[%key:common::time::wednesday%]",
"thursday": "[%key:common::time::thursday%]",
"friday": "[%key:common::time::friday%]",
"saturday": "[%key:common::time::saturday%]",
"sunday": "[%key:common::time::sunday%]"
}
}
}
}
}

View File

@@ -111,12 +111,12 @@
},
"issues": {
"deprecated_service_calendar_list_events": {
"title": "Detected use of deprecated action `calendar.list_events`",
"title": "Detected use of deprecated service `calendar.list_events`",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::calendar::issues::deprecated_service_calendar_list_events::title%]",
"description": "Use `calendar.get_events` instead which supports multiple entities.\n\nPlease replace this action and adjust your automations and scripts and select **submit** to close this issue."
"description": "Use `calendar.get_events` instead which supports multiple entities.\n\nPlease replace this service and adjust your automations and scripts and select **submit** to close this issue."
}
}
}

View File

@@ -7,15 +7,18 @@ import numpy as np
import voluptuous as vol
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_MAXIMUM,
CONF_MINIMUM,
CONF_NAME,
CONF_SOURCE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.typing import ConfigType
@@ -32,6 +35,7 @@ from .const import (
DEFAULT_DEGREE,
DEFAULT_PRECISION,
DOMAIN,
PLATFORMS,
)
_LOGGER = logging.getLogger(__name__)
@@ -77,59 +81,104 @@ CONFIG_SCHEMA = vol.Schema(
)
async def create_compensation_data(
hass: HomeAssistant, compensation: str, conf: ConfigType, should_raise: bool = False
) -> None:
"""Create compensation data."""
_LOGGER.debug("Setup %s.%s", DOMAIN, compensation)
degree = conf[CONF_DEGREE]
initial_coefficients: list[tuple[float, float]] = conf[CONF_DATAPOINTS]
sorted_coefficients = sorted(initial_coefficients, key=itemgetter(0))
# get x values and y values from the x,y point pairs
x_values, y_values = zip(*initial_coefficients, strict=False)
# try to get valid coefficients for a polynomial
coefficients = None
with np.errstate(all="raise"):
try:
coefficients = np.polyfit(x_values, y_values, degree)
except FloatingPointError as error:
_LOGGER.error(
"Setup of %s encountered an error, %s",
compensation,
error,
)
if should_raise:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="setup_error",
translation_placeholders={
"title": conf[CONF_NAME],
"error": str(error),
},
) from error
if coefficients is not None:
data = {
k: v for k, v in conf.items() if k not in [CONF_DEGREE, CONF_DATAPOINTS]
}
data[CONF_POLYNOMIAL] = np.poly1d(coefficients)
if data[CONF_LOWER_LIMIT]:
data[CONF_MINIMUM] = sorted_coefficients[0]
else:
data[CONF_MINIMUM] = None
if data[CONF_UPPER_LIMIT]:
data[CONF_MAXIMUM] = sorted_coefficients[-1]
else:
data[CONF_MAXIMUM] = None
hass.data[DATA_COMPENSATION][compensation] = data
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Compensation sensor."""
hass.data[DATA_COMPENSATION] = {}
if DOMAIN not in config:
return True
for compensation, conf in config[DOMAIN].items():
_LOGGER.debug("Setup %s.%s", DOMAIN, compensation)
degree = conf[CONF_DEGREE]
initial_coefficients: list[tuple[float, float]] = conf[CONF_DATAPOINTS]
sorted_coefficients = sorted(initial_coefficients, key=itemgetter(0))
# get x values and y values from the x,y point pairs
x_values, y_values = zip(*initial_coefficients, strict=False)
# try to get valid coefficients for a polynomial
coefficients = None
with np.errstate(all="raise"):
try:
coefficients = np.polyfit(x_values, y_values, degree)
except FloatingPointError as error:
_LOGGER.error(
"Setup of %s encountered an error, %s",
compensation,
error,
)
if coefficients is not None:
data = {
k: v for k, v in conf.items() if k not in [CONF_DEGREE, CONF_DATAPOINTS]
}
data[CONF_POLYNOMIAL] = np.poly1d(coefficients)
if data[CONF_LOWER_LIMIT]:
data[CONF_MINIMUM] = sorted_coefficients[0]
else:
data[CONF_MINIMUM] = None
if data[CONF_UPPER_LIMIT]:
data[CONF_MAXIMUM] = sorted_coefficients[-1]
else:
data[CONF_MAXIMUM] = None
hass.data[DATA_COMPENSATION][compensation] = data
hass.async_create_task(
async_load_platform(
hass,
SENSOR_DOMAIN,
DOMAIN,
{CONF_COMPENSATION: compensation},
config,
)
await create_compensation_data(hass, compensation, conf)
hass.async_create_task(
async_load_platform(
hass,
SENSOR_DOMAIN,
DOMAIN,
{CONF_COMPENSATION: compensation},
config,
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Compensation from a config entry."""
config = dict(entry.options)
data_points = config[CONF_DATAPOINTS]
new_data_points = []
for data_point in data_points:
values = data_point.split(",", maxsplit=1)
new_data_points.append([float(values[0]), float(values[1])])
config[CONF_DATAPOINTS] = new_data_points
await create_compensation_data(hass, entry.entry_id, config, True)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Compensation config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -0,0 +1,147 @@
"""Config flow for statistics."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, cast
import voluptuous as vol
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_ENTITY_ID,
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
)
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaConfigFlowHandler,
SchemaFlowError,
SchemaFlowFormStep,
)
from homeassistant.helpers.selector import (
AttributeSelector,
AttributeSelectorConfig,
BooleanSelector,
EntitySelector,
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
)
from .const import (
CONF_DATAPOINTS,
CONF_DEGREE,
CONF_LOWER_LIMIT,
CONF_PRECISION,
CONF_UPPER_LIMIT,
DEFAULT_DEGREE,
DEFAULT_NAME,
DEFAULT_PRECISION,
DOMAIN,
)
async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
"""Get options schema."""
entity_id = handler.options[CONF_ENTITY_ID]
return vol.Schema(
{
vol.Required(CONF_DATAPOINTS): SelectSelector(
SelectSelectorConfig(
options=[],
multiple=True,
custom_value=True,
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(CONF_ATTRIBUTE): AttributeSelector(
AttributeSelectorConfig(entity_id=entity_id)
),
vol.Optional(CONF_UPPER_LIMIT, default=False): BooleanSelector(),
vol.Optional(CONF_LOWER_LIMIT, default=False): BooleanSelector(),
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): NumberSelector(
NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX)
),
vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): NumberSelector(
NumberSelectorConfig(min=0, max=7, step=1, mode=NumberSelectorMode.BOX)
),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): TextSelector(),
}
)
def _is_valid_data_points(check_data_points: list[str]) -> bool:
"""Validate data points."""
result = False
for data_point in check_data_points:
if not data_point.find(",") > 0:
return False
values = data_point.split(",", maxsplit=1)
for value in values:
try:
float(value)
except ValueError:
return False
result = True
return result
async def validate_options(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate options selected."""
user_input[CONF_PRECISION] = int(user_input[CONF_PRECISION])
user_input[CONF_DEGREE] = int(user_input[CONF_DEGREE])
if not _is_valid_data_points(user_input[CONF_DATAPOINTS]):
raise SchemaFlowError("incorrect_datapoints")
if len(user_input[CONF_DATAPOINTS]) <= user_input[CONF_DEGREE]:
raise SchemaFlowError("not_enough_datapoints")
handler.parent_handler._async_abort_entries_match({**handler.options, **user_input}) # noqa: SLF001
return user_input
DATA_SCHEMA_SETUP = vol.Schema(
{
vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
vol.Required(CONF_ENTITY_ID): EntitySelector(),
}
)
CONFIG_FLOW = {
"user": SchemaFlowFormStep(
schema=DATA_SCHEMA_SETUP,
next_step="options",
),
"options": SchemaFlowFormStep(
schema=get_options_schema,
validate_user_input=validate_options,
),
}
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(
get_options_schema,
validate_user_input=validate_options,
),
}
class CompensationConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for Compensation."""
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""
return cast(str, options[CONF_NAME])

View File

@@ -1,6 +1,9 @@
"""Compensation constants."""
from homeassistant.const import Platform
DOMAIN = "compensation"
PLATFORMS = [Platform.SENSOR]
SENSOR = "compensation"

View File

@@ -2,7 +2,9 @@
"domain": "compensation",
"name": "Compensation",
"codeowners": ["@Petro31"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/compensation",
"integration_type": "helper",
"iot_class": "calculated",
"requirements": ["numpy==1.26.0"]
}

View File

@@ -8,9 +8,11 @@ from typing import Any
import numpy as np
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONF_ATTRIBUTE,
CONF_ENTITY_ID,
CONF_MAXIMUM,
CONF_MINIMUM,
CONF_SOURCE,
@@ -80,6 +82,36 @@ async def async_setup_platform(
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Compensation sensor entry."""
compensation = entry.entry_id
conf: dict[str, Any] = hass.data[DATA_COMPENSATION][compensation]
source: str = conf[CONF_ENTITY_ID]
attribute: str | None = conf.get(CONF_ATTRIBUTE)
name = entry.title
async_add_entities(
[
CompensationSensor(
entry.entry_id,
name,
source,
attribute,
conf[CONF_PRECISION],
conf[CONF_POLYNOMIAL],
conf.get(CONF_UNIT_OF_MEASUREMENT),
conf[CONF_MINIMUM],
conf[CONF_MAXIMUM],
)
]
)
class CompensationSensor(SensorEntity):
"""Representation of a Compensation sensor."""

View File

@@ -0,0 +1,82 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"incorrect_datapoints": "Datapoints needs to be provided in the right format, ex. '1.0, 0.0'.",
"not_enough_datapoints": "The number of datapoints needs to be more than the configured degree."
},
"step": {
"user": {
"description": "Add a compensation sensor",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"entity_id": "Entity"
},
"data_description": {
"name": "Name for the created entity.",
"entity_id": "Entity to use as source."
}
},
"options": {
"description": "Read the documention for further details on how to configure the statistics sensor using these options.",
"data": {
"data_points": "Data points",
"attribute": "Attribute",
"upper_limit": "Upper limit",
"lower_limit": "Lower limit",
"precision": "Precision",
"degree": "Degree",
"unit_of_measurement": "Unit of measurement"
},
"data_description": {
"data_points": "The collection of data point conversions with the format 'uncompensated_value, compensated_value', ex. '1.0, 0.0'",
"attribute": "Attribute from the source to monitor/compensate.",
"upper_limit": "Enables an upper limit for the sensor. The upper limit is defined by the data collections (data_points) greatest uncompensated value.",
"lower_limit": "Enables a lower limit for the sensor. The lower limit is defined by the data collections (data_points) lowest uncompensated value.",
"precision": "Defines the precision of the calculated values, through the argument of round().",
"degree": "The degree of a polynomial.",
"unit_of_measurement": "Defines the units of measurement of the sensor, if any."
}
}
}
},
"options": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"incorrect_datapoints": "[%key:component::compensation::config::error::incorrect_datapoints%]",
"not_enough_datapoints": "[%key:component::compensation::config::error::not_enough_datapoints%]"
},
"step": {
"init": {
"description": "[%key:component::compensation::config::step::options::description%]",
"data": {
"data_points": "[%key:component::compensation::config::step::options::data::data_points%]",
"attribute": "[%key:component::compensation::config::step::options::data::attribute%]",
"upper_limit": "[%key:component::compensation::config::step::options::data::upper_limit%]",
"lower_limit": "[%key:component::compensation::config::step::options::data::lower_limit%]",
"precision": "[%key:component::compensation::config::step::options::data::precision%]",
"degree": "[%key:component::compensation::config::step::options::data::degree%]",
"unit_of_measurement": "[%key:component::compensation::config::step::options::data::unit_of_measurement%]"
},
"data_description": {
"data_points": "[%key:component::compensation::config::step::options::data_description::data_points%]",
"attribute": "[%key:component::compensation::config::step::options::data_description::attribute%]",
"upper_limit": "[%key:component::compensation::config::step::options::data_description::upper_limit%]",
"lower_limit": "[%key:component::compensation::config::step::options::data_description::lower_limit%]",
"precision": "[%key:component::compensation::config::step::options::data_description::precision%]",
"degree": "[%key:component::compensation::config::step::options::data_description::degree%]",
"unit_of_measurement": "[%key:component::compensation::config::step::options::data_description::unit_of_measurement%]"
}
}
}
},
"exceptions": {
"setup_error": {
"message": "Setup of {title} could not be setup due to {error}"
}
}
}

View File

@@ -1,12 +1,11 @@
"""Support for Concord232 alarm control panels."""
# mypy: ignore-errors
from __future__ import annotations
import datetime
import logging
# from concord232 import client as concord232_client
from concord232 import client as concord232_client
import requests
import voluptuous as vol

View File

@@ -1,12 +1,11 @@
"""Support for exposing Concord232 elements as sensors."""
# mypy: ignore-errors
from __future__ import annotations
import datetime
import logging
# from concord232 import client as concord232_client
from concord232 import client as concord232_client
import requests
import voluptuous as vol

View File

@@ -2,7 +2,6 @@
"domain": "concord232",
"name": "Concord232",
"codeowners": [],
"disabled": "This integration is disabled because it uses non-open source code to operate.",
"documentation": "https://www.home-assistant.io/integrations/concord232",
"iot_class": "local_polling",
"loggers": ["concord232", "stevedore"],

View File

@@ -1,5 +0,0 @@
extend = "../../../pyproject.toml"
lint.extend-ignore = [
"F821"
]

View File

@@ -40,7 +40,6 @@ class DevoloDeviceEntity(Entity):
identifiers={(DOMAIN, self._device_instance.uid)},
manufacturer=device_instance.brand,
model=device_instance.name,
model_id=device_instance.identifier,
name=device_instance.settings_property["general_device_settings"].name,
suggested_area=device_instance.settings_property[
"general_device_settings"

View File

@@ -48,7 +48,6 @@ class DevoloEntity(Entity):
identifiers={(DOMAIN, str(self.device.serial_number))},
manufacturer="devolo",
model=self.device.product,
model_id=self.device.mt_number,
serial_number=self.device.serial_number,
sw_version=self.device.firmware_version,
)

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from http import HTTPStatus
import logging
from aiohttp import ClientResponseError
from doorbirdpy import DoorBird
@@ -16,7 +17,7 @@ from homeassistant.const import (
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -26,6 +27,8 @@ from .device import ConfiguredDoorBird
from .models import DoorBirdConfigEntry, DoorBirdData
from .view import DoorBirdRequestView
_LOGGER = logging.getLogger(__name__)
CONF_CUSTOM_URL = "hass_url_override"
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@@ -49,14 +52,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) ->
device = DoorBird(device_ip, username, password, http_session=session)
try:
status = await device.ready()
info = await device.info()
except ClientResponseError as err:
if err.status == HTTPStatus.UNAUTHORIZED:
raise ConfigEntryAuthFailed from err
_LOGGER.error(
"Authorization rejected by DoorBird for %s@%s", username, device_ip
)
return False
raise ConfigEntryNotReady from err
except OSError as oserr:
_LOGGER.error("Failed to setup doorbird at %s: %s", device_ip, oserr)
raise ConfigEntryNotReady from oserr
if not status[0]:
_LOGGER.error(
"Could not connect to DoorBird as %s@%s: Error %s",
username,
device_ip,
str(status[1]),
)
raise ConfigEntryNotReady
token: str = door_station_config.get(CONF_TOKEN, config_entry_id)
custom_url: str | None = door_station_config.get(CONF_CUSTOM_URL)
name: str | None = door_station_config.get(CONF_NAME)

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Mapping
from http import HTTPStatus
import logging
from typing import Any
@@ -22,7 +21,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNA
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import VolDictType
from .const import (
CONF_EVENTS,
@@ -38,20 +36,14 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_OPTIONS = {CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]}
AUTH_VOL_DICT: VolDictType = {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
AUTH_SCHEMA = vol.Schema(AUTH_VOL_DICT)
def _schema_with_defaults(
host: str | None = None, name: str | None = None
) -> vol.Schema:
return vol.Schema(
{
vol.Required(CONF_HOST, default=host): str,
**AUTH_VOL_DICT,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_NAME, default=name): str,
}
)
@@ -64,6 +56,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], http_session=session
)
try:
status = await device.ready()
info = await device.info()
except ClientResponseError as err:
if err.status == HTTPStatus.UNAUTHORIZED:
@@ -72,6 +65,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
except OSError as err:
raise CannotConnect from err
if not status[0]:
raise CannotConnect
mac_addr = get_mac_address_from_door_station_info(info)
# Return info that you want to store in the config entry.
@@ -100,47 +96,6 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the DoorBird config flow."""
self.discovery_schema: vol.Schema | None = None
self.reauth_entry: ConfigEntry | None = None
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth."""
entry_id = self.context["entry_id"]
self.reauth_entry = self.hass.config_entries.async_get_entry(entry_id)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauth input."""
errors: dict[str, str] = {}
existing_entry = self.reauth_entry
assert existing_entry
existing_data = existing_entry.data
placeholders: dict[str, str] = {
CONF_NAME: existing_data[CONF_NAME],
CONF_HOST: existing_data[CONF_HOST],
}
self.context["title_placeholders"] = placeholders
if user_input is not None:
new_config = {
**existing_data,
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
_, errors = await self._async_validate_or_error(new_config)
if not errors:
return self.async_update_reload_and_abort(
existing_entry, data=new_config
)
return self.async_show_form(
description_placeholders=placeholders,
step_id="reauth_confirm",
data_schema=AUTH_SCHEMA,
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None

View File

@@ -4,6 +4,9 @@ from homeassistant.const import Platform
DOMAIN = "doorbird"
PLATFORMS = [Platform.BUTTON, Platform.CAMERA, Platform.EVENT]
DOOR_STATION = "door_station"
DOOR_STATION_INFO = "door_station_info"
DOOR_STATION_EVENT_ENTITY_IDS = "door_station_event_entity_ids"
CONF_EVENTS = "events"
MANUFACTURER = "Bird Home Automation Group"

View File

@@ -7,8 +7,7 @@ from homeassistant.components.event import (
EventEntity,
EventEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
@@ -71,15 +70,14 @@ class DoorBirdEventEntity(DoorBirdEntity, EventEntity):
async def async_added_to_hass(self) -> None:
"""Subscribe to device events."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
self.hass.bus.async_listen(
f"{DOMAIN}_{self._doorbird_event.event}",
self._async_handle_event,
)
)
@callback
def _async_handle_event(self) -> None:
def _async_handle_event(self, event: Event) -> None:
"""Handle a device event."""
event_types = self.entity_description.event_types
if TYPE_CHECKING:

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/doorbird",
"iot_class": "local_push",
"loggers": ["doorbirdpy"],
"requirements": ["DoorBirdPy==3.0.2"],
"requirements": ["DoorBirdPy==3.0.1"],
"zeroconf": [
{
"type": "_axis-video._tcp.local.",

View File

@@ -23,20 +23,12 @@
"data_description": {
"host": "The hostname or IP address of your DoorBird device."
}
},
"reauth_confirm": {
"description": "Re-authenticate DoorBird device {name} at {host}",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"link_local_address": "Link local addresses are not supported",
"not_doorbird_device": "This device is not a DoorBird",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"not_doorbird_device": "This device is not a DoorBird"
},
"flow_title": "{name} ({host})",
"error": {

View File

@@ -7,7 +7,6 @@ from http import HTTPStatus
from aiohttp import web
from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import API_URL, DOMAIN
from .util import get_door_station_by_token
@@ -46,7 +45,5 @@ class DoorBirdRequestView(HomeAssistantView):
# Do not copy this pattern in the future
# for any new integrations.
#
event_type = f"{DOMAIN}_{event}"
hass.bus.async_fire(event_type, event_data)
async_dispatcher_send(hass, event_type)
hass.bus.async_fire(f"{DOMAIN}_{event}", event_data)
return web.Response(text="OK")

View File

@@ -1,10 +1,9 @@
"""Support for Dovado router."""
# mypy: ignore-errors
from datetime import timedelta
import logging
# import dovado
import dovado
import voluptuous as vol
from homeassistant.const import (

View File

@@ -2,7 +2,6 @@
"domain": "dovado",
"name": "Dovado",
"codeowners": [],
"disabled": "This integration is disabled because it uses non-open source code to operate.",
"documentation": "https://www.home-assistant.io/integrations/dovado",
"iot_class": "local_polling",
"requirements": ["dovado==0.4.1"]

View File

@@ -1,5 +0,0 @@
extend = "../../../pyproject.toml"
lint.extend-ignore = [
"F821"
]

View File

@@ -1,6 +1,6 @@
{
"domain": "dsmr",
"name": "DSMR Smart Meter",
"name": "DSMR Slimme Meter",
"codeowners": ["@Robbie1221", "@frenck"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dsmr",

View File

@@ -16,7 +16,7 @@ from dsmr_parser.clients.rfxtrx_protocol import (
create_rfxtrx_dsmr_reader,
create_rfxtrx_tcp_dsmr_reader,
)
from dsmr_parser.objects import DSMRObject, Telegram
from dsmr_parser.objects import DSMRObject
import serial
from homeassistant.components.sensor import (
@@ -380,7 +380,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
def create_mbus_entity(
mbus: int, mtype: int, telegram: Telegram
mbus: int, mtype: int, telegram: dict[str, DSMRObject]
) -> DSMRSensorEntityDescription | None:
"""Create a new MBUS Entity."""
if (
@@ -478,7 +478,7 @@ def rename_old_gas_to_mbus(
def create_mbus_entities(
hass: HomeAssistant, telegram: Telegram, entry: ConfigEntry
hass: HomeAssistant, telegram: dict[str, DSMRObject], entry: ConfigEntry
) -> list[DSMREntity]:
"""Create MBUS Entities."""
entities = []
@@ -523,7 +523,7 @@ async def async_setup_entry(
add_entities_handler: Callable[..., None] | None
@callback
def init_async_add_entities(telegram: Telegram) -> None:
def init_async_add_entities(telegram: dict[str, DSMRObject]) -> None:
"""Add the sensor entities after the first telegram was received."""
nonlocal add_entities_handler
assert add_entities_handler is not None
@@ -560,7 +560,7 @@ async def async_setup_entry(
)
@Throttle(min_time_between_updates)
def update_entities_telegram(telegram: Telegram | None) -> None:
def update_entities_telegram(telegram: dict[str, DSMRObject] | None) -> None:
"""Update entities with latest telegram and trigger state update."""
nonlocal initialized
# Make all device entities aware of new telegram
@@ -709,7 +709,7 @@ class DSMREntity(SensorEntity):
self,
entity_description: DSMRSensorEntityDescription,
entry: ConfigEntry,
telegram: Telegram,
telegram: dict[str, DSMRObject],
device_class: SensorDeviceClass,
native_unit_of_measurement: str | None,
serial_id: str = "",
@@ -720,7 +720,7 @@ class DSMREntity(SensorEntity):
self._attr_device_class = device_class
self._attr_native_unit_of_measurement = native_unit_of_measurement
self._entry = entry
self.telegram: Telegram | None = telegram
self.telegram: dict[str, DSMRObject] | None = telegram
device_serial = entry.data[CONF_SERIAL_ID]
device_name = DEVICE_NAME_ELECTRICITY
@@ -750,7 +750,7 @@ class DSMREntity(SensorEntity):
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
@callback
def update_data(self, telegram: Telegram | None) -> None:
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None:
"""Update data."""
self.telegram = telegram
if self.hass and (

View File

@@ -171,12 +171,12 @@
},
"issues": {
"migrate_aux_heat": {
"title": "Migration of Ecobee set_aux_heat action",
"title": "Migration of Ecobee set_aux_heat service",
"fix_flow": {
"step": {
"confirm": {
"description": "The Ecobee `set_aux_heat` action has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.",
"title": "Disable legacy Ecobee set_aux_heat action"
"description": "The Ecobee `set_aux_heat` service has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.",
"title": "Disable legacy Ecobee set_aux_heat service"
}
}
}

View File

@@ -58,7 +58,7 @@
"fields": {
"config_entry": {
"name": "Config Entry",
"description": "The config entry to use for this action."
"description": "The config entry to use for this service."
},
"incl_vat": {
"name": "Including VAT",

View File

@@ -199,8 +199,7 @@ class Enigma2Device(MediaPlayerEntity):
async def async_mute_volume(self, mute: bool) -> None:
"""Mute or unmute."""
if mute != self._device.status.muted:
await self._device.toggle_mute()
await self._device.toggle_mute()
async def async_select_source(self, source: str) -> None:
"""Select input source."""

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.7.2"]
"requirements": ["env-canada==0.7.1"]
}

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
import datetime
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
@@ -35,6 +37,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from . import device_info
from .const import DOMAIN
@@ -190,24 +193,53 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None:
if not (half_days := ec_data.daily_forecasts):
return None
def get_day_forecast(fcst: list[dict[str, str]]) -> Forecast:
high_temp = int(fcst[0]["temperature"]) if len(fcst) == 2 else None
return {
ATTR_FORECAST_TIME: fcst[0]["timestamp"],
ATTR_FORECAST_NATIVE_TEMP: high_temp,
ATTR_FORECAST_NATIVE_TEMP_LOW: int(fcst[-1]["temperature"]),
ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(
fcst[0]["precip_probability"]
),
ATTR_FORECAST_CONDITION: icon_code_to_condition(
int(fcst[0]["icon_code"])
),
}
today: Forecast = {
ATTR_FORECAST_TIME: dt_util.now().isoformat(),
ATTR_FORECAST_CONDITION: icon_code_to_condition(
int(half_days[0]["icon_code"])
),
ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(
half_days[0]["precip_probability"]
),
}
i = 2 if half_days[0]["temperature_class"] == "high" else 1
forecast_array.append(get_day_forecast(half_days[0:i]))
for i in range(i, len(half_days) - 1, 2):
forecast_array.append(get_day_forecast(half_days[i : i + 2])) # noqa: PERF401
if half_days[0]["temperature_class"] == "high":
today.update(
{
ATTR_FORECAST_NATIVE_TEMP: int(half_days[0]["temperature"]),
ATTR_FORECAST_NATIVE_TEMP_LOW: int(half_days[1]["temperature"]),
}
)
half_days = half_days[2:]
else:
today.update(
{
ATTR_FORECAST_NATIVE_TEMP: None,
ATTR_FORECAST_NATIVE_TEMP_LOW: int(half_days[0]["temperature"]),
}
)
half_days = half_days[1:]
forecast_array.append(today)
for day, high, low in zip(
range(1, 6), range(0, 9, 2), range(1, 10, 2), strict=False
):
forecast_array.append(
{
ATTR_FORECAST_TIME: (
dt_util.now() + datetime.timedelta(days=day)
).isoformat(),
ATTR_FORECAST_NATIVE_TEMP: int(half_days[high]["temperature"]),
ATTR_FORECAST_NATIVE_TEMP_LOW: int(half_days[low]["temperature"]),
ATTR_FORECAST_CONDITION: icon_code_to_condition(
int(half_days[high]["icon_code"])
),
ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(
half_days[high]["precip_probability"]
),
}
)
else:
forecast_array.extend(

View File

@@ -5,7 +5,7 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"mdns_missing_mac": "Missing MAC address in MDNS properties.",
"service_received": "Action received",
"service_received": "Service received",
"mqtt_missing_mac": "Missing MAC address in MQTT properties.",
"mqtt_missing_api": "Missing API port in MQTT properties.",
"mqtt_missing_ip": "Missing IP address in MQTT properties."
@@ -53,7 +53,7 @@
"step": {
"init": {
"data": {
"allow_service_calls": "Allow the device to perform Home Assistant actions."
"allow_service_calls": "Allow the device to make Home Assistant service calls."
}
}
}
@@ -102,8 +102,8 @@
"description": "The API password for ESPHome is deprecated and the use of an API encryption key is recommended instead.\n\nRemove the API password and add an encryption key to your ESPHome device to resolve this issue."
},
"service_calls_not_allowed": {
"title": "{name} is not permitted to perform Home Assistant actions",
"description": "The ESPHome device attempted to perform a Home Assistant action, but this functionality is not enabled.\n\nIf you trust this device and want to allow it to perfom Home Assistant action, you can enable this functionality in the options flow."
"title": "{name} is not permitted to call Home Assistant services",
"description": "The ESPHome device attempted to make a Home Assistant service call, but this functionality is not enabled.\n\nIf you trust this device and want to allow it to make Home Assistant service calls, you can enable this functionality in the options flow."
}
}
}

View File

@@ -4,7 +4,6 @@
"codeowners": ["@dgomes"],
"dependencies": ["recorder"],
"documentation": "https://www.home-assistant.io/integrations/filter",
"integration_type": "helper",
"iot_class": "local_push",
"quality_scale": "internal"
}

View File

@@ -28,7 +28,7 @@
"services": {
"ptz": {
"name": "PTZ",
"description": "Pan/Tilt action for Foscam camera.",
"description": "Pan/Tilt service for Foscam camera.",
"fields": {
"movement": {
"name": "Movement",
@@ -42,7 +42,7 @@
},
"ptz_preset": {
"name": "PTZ preset",
"description": "PTZ Preset action for Foscam camera.",
"description": "PTZ Preset service for Foscam camera.",
"fields": {
"preset_name": {
"name": "Preset name",

View File

@@ -165,10 +165,10 @@
},
"exceptions": {
"config_entry_not_found": {
"message": "Failed to perform action \"{service}\". Config entry for target not found"
"message": "Failed to call service \"{service}\". Config entry for target not found"
},
"service_parameter_unknown": { "message": "Action or parameter unknown" },
"service_not_supported": { "message": "Action not supported" },
"service_parameter_unknown": { "message": "Service or parameter unknown" },
"service_not_supported": { "message": "Service not supported" },
"error_refresh_hosts_info": {
"message": "Error refreshing hosts info"
},

View File

@@ -13,7 +13,7 @@
"fields": {
"agent_user_id": {
"name": "Agent user ID",
"description": "Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you use this action through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing."
"description": "Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing."
}
}
}

View File

@@ -14,26 +14,6 @@
"local_name": "B5178*",
"connectable": false
},
{
"local_name": "GV5121*",
"connectable": false
},
{
"local_name": "GV5122*",
"connectable": false
},
{
"local_name": "GV5123*",
"connectable": false
},
{
"local_name": "GV5125*",
"connectable": false
},
{
"local_name": "GV5126*",
"connectable": false
},
{
"manufacturer_id": 1,
"service_uuid": "0000ec88-0000-1000-8000-00805f9b34fb",
@@ -103,10 +83,6 @@
"manufacturer_id": 19506,
"service_uuid": "00001801-0000-1000-8000-00805f9b34fb",
"connectable": false
},
{
"manufacturer_id": 61320,
"connectable": false
}
],
"codeowners": ["@bdraco", "@PierreAronnax"],
@@ -114,5 +90,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/govee_ble",
"iot_class": "local_push",
"requirements": ["govee-ble==0.37.0"]
"requirements": ["govee-ble==0.31.3"]
}

View File

@@ -194,7 +194,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
- Remove group.group entities not created by service calls and set them up again
- Reload xxx.group platforms
"""
conf = await component.async_prepare_reload(skip_reset=True)
if (conf := await component.async_prepare_reload(skip_reset=True)) is None:
return
# Simplified + modified version of EntityPlatform.async_reset:
# - group.group never retries setup

View File

@@ -11,7 +11,7 @@
"user": {
"data": {
"url": "[%key:common::config_flow::data::url%]",
"name": "Override for Habiticas username. Will be used for actions",
"name": "Override for Habiticas username. Will be used for service calls",
"api_user": "Habiticas API user ID",
"api_key": "[%key:common::config_flow::data::api_key%]"
},

View File

@@ -289,7 +289,7 @@
},
"addon_update": {
"name": "Update add-on.",
"description": "Updates an add-on. This action should be used with caution since add-on updates can contain breaking changes. It is highly recommended that you review release notes/change logs before updating an add-on.",
"description": "Updates an add-on. This service should be used with caution since add-on updates can contain breaking changes. It is highly recommended that you review release notes/change logs before updating an add-on.",
"fields": {
"addon": {
"name": "[%key:component::hassio::services::addon_start::fields::addon::name%]",

View File

@@ -7,9 +7,6 @@ from datetime import timedelta
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID, CONF_STATE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import (
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.template import Template
from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS
@@ -45,12 +42,6 @@ async def async_setup_entry(
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
async_remove_stale_devices_links_keep_entity_device(
hass,
entry.entry_id,
entry.options[CONF_ENTITY_ID],
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))

View File

@@ -26,7 +26,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device import async_device_info_to_link_from_entity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.template import Template
@@ -112,9 +111,7 @@ async def async_setup_platform(
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise PlatformNotReady from coordinator.last_exception
async_add_entities(
[HistoryStatsSensor(hass, coordinator, sensor_type, name, unique_id, entity_id)]
)
async_add_entities([HistoryStatsSensor(coordinator, sensor_type, name, unique_id)])
async def async_setup_entry(
@@ -126,13 +123,8 @@ async def async_setup_entry(
sensor_type: str = entry.options[CONF_TYPE]
coordinator = entry.runtime_data
entity_id: str = entry.options[CONF_ENTITY_ID]
async_add_entities(
[
HistoryStatsSensor(
hass, coordinator, sensor_type, entry.title, entry.entry_id, entity_id
)
]
[HistoryStatsSensor(coordinator, sensor_type, entry.title, entry.entry_id)]
)
@@ -175,22 +167,16 @@ class HistoryStatsSensor(HistoryStatsSensorBase):
def __init__(
self,
hass: HomeAssistant,
coordinator: HistoryStatsUpdateCoordinator,
sensor_type: str,
name: str,
unique_id: str | None,
source_entity_id: str,
) -> None:
"""Initialize the HistoryStats sensor."""
super().__init__(coordinator, name)
self._attr_native_unit_of_measurement = UNITS[sensor_type]
self._type = sensor_type
self._attr_unique_id = unique_id
self._attr_device_info = async_device_info_to_link_from_entity(
hass,
source_entity_id,
)
self._process_update()
if self._type == CONF_TYPE_TIME:
self._attr_device_class = SensorDeviceClass.DURATION

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.53", "babel==2.15.0"]
"requirements": ["holidays==0.52", "babel==2.15.0"]
}

View File

@@ -133,15 +133,15 @@
},
"toggle": {
"name": "Generic toggle",
"description": "Generic action to toggle devices on/off under any domain."
"description": "Generic service to toggle devices on/off under any domain."
},
"turn_on": {
"name": "Generic turn on",
"description": "Generic action to turn devices on under any domain."
"description": "Generic service to turn devices on under any domain."
},
"turn_off": {
"name": "Generic turn off",
"description": "Generic action to turn devices off under any domain."
"description": "Generic service to turn devices off under any domain."
},
"update_entity": {
"name": "Update entity",
@@ -205,19 +205,19 @@
"message": "Unknown error when validating config for {domain} from integration {p_name} - {error}."
},
"service_not_found": {
"message": "Action {domain}.{service} not found."
"message": "Service {domain}.{service} not found."
},
"service_does_not_support_response": {
"message": "An action which does not return responses can't be called with {return_response}."
"message": "A service which does not return responses can't be called with {return_response}."
},
"service_lacks_response_request": {
"message": "The action requires responses and must be called with {return_response}."
"message": "The service call requires responses and must be called with {return_response}."
},
"service_reponse_invalid": {
"message": "Failed to process the returned action response data, expected a dictionary, but got {response_data_type}."
"message": "Failed to process the returned service response data, expected a dictionary, but got {response_data_type}."
},
"service_should_be_blocking": {
"message": "A non blocking action call with argument {non_blocking_argument} can't be used together with argument {return_response}."
"message": "A non blocking service call with argument {non_blocking_argument} can't be used together with argument {return_response}."
}
}
}

View File

@@ -80,7 +80,7 @@
},
"unpair": {
"name": "Unpair an accessory or bridge",
"description": "Forcefully removes all pairings from an accessory to allow re-pairing. Use this action if the accessory is no longer responsive, and you want to avoid deleting and re-adding the entry. Room locations, and accessory preferences will be lost."
"description": "Forcefully removes all pairings from an accessory to allow re-pairing. Use this service if the accessory is no longer responsive, and you want to avoid deleting and re-adding the entry. Room locations, and accessory preferences will be lost."
}
}
}

View File

@@ -233,7 +233,8 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
self._char_motion_detected = serv_motion.configure_char(
CHAR_MOTION_DETECTED, value=False
)
self._async_update_motion_state(None, state)
if not self.motion_is_event:
self._async_update_motion_state(state)
self._char_doorbell_detected = None
self._char_doorbell_detected_switch = None
@@ -263,7 +264,9 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
)
serv_speaker = self.add_preload_service(SERV_SPEAKER)
serv_speaker.configure_char(CHAR_MUTE, value=0)
self._async_update_doorbell_state(None, state)
if not self.doorbell_is_event:
self._async_update_doorbell_state(state)
@pyhap_callback # type: ignore[misc]
@callback
@@ -301,25 +304,20 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
self, event: Event[EventStateChangedData]
) -> None:
"""Handle state change event listener callback."""
if not state_changed_event_is_same_state(event) and (
new_state := event.data["new_state"]
):
self._async_update_motion_state(event.data["old_state"], new_state)
if not state_changed_event_is_same_state(event):
self._async_update_motion_state(event.data["new_state"])
@callback
def _async_update_motion_state(
self, old_state: State | None, new_state: State
) -> None:
def _async_update_motion_state(self, new_state: State | None) -> None:
"""Handle link motion sensor state change to update HomeKit value."""
if not new_state:
return
state = new_state.state
char = self._char_motion_detected
assert char is not None
if self.motion_is_event:
if (
old_state is None
or old_state.state == STATE_UNAVAILABLE
or state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
):
if state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
return
_LOGGER.debug(
"%s: Set linked motion %s sensor to True/False",
@@ -350,21 +348,16 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
if not state_changed_event_is_same_state(event) and (
new_state := event.data["new_state"]
):
self._async_update_doorbell_state(event.data["old_state"], new_state)
self._async_update_doorbell_state(new_state)
@callback
def _async_update_doorbell_state(
self, old_state: State | None, new_state: State
) -> None:
def _async_update_doorbell_state(self, new_state: State) -> None:
"""Handle link doorbell sensor state change to update HomeKit value."""
assert self._char_doorbell_detected
assert self._char_doorbell_detected_switch
state = new_state.state
if state == STATE_ON or (
self.doorbell_is_event
and old_state is not None
and old_state.state != STATE_UNAVAILABLE
and state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
self.doorbell_is_event and state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
):
self._char_doorbell_detected.set_value(DOORBELL_SINGLE_PRESS)
self._char_doorbell_detected_switch.set_value(DOORBELL_SINGLE_PRESS)

View File

@@ -361,7 +361,7 @@
},
"suspend_integration": {
"name": "Suspend integration",
"description": "Suspends integration. Suspending logs the integration out from the router, and stops accessing it. Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. Invoke the resume_integration action to resume.\n.",
"description": "Suspends integration. Suspending logs the integration out from the router, and stops accessing it. Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. Invoke the resume_integration service to resume.\n.",
"fields": {
"url": {
"name": "[%key:common::config_flow::data::url%]",

View File

@@ -9,6 +9,7 @@ from aiopvapi.rooms import Rooms
from aiopvapi.scenes import Scenes
from aiopvapi.shades import Shades
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_VERSION, CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@@ -17,7 +18,7 @@ import homeassistant.helpers.config_validation as cv
from .const import DOMAIN, HUB_EXCEPTIONS
from .coordinator import PowerviewShadeUpdateCoordinator
from .model import PowerviewConfigEntry, PowerviewDeviceInfo, PowerviewEntryData
from .model import PowerviewDeviceInfo, PowerviewEntryData
from .shade_data import PowerviewShadeData
PARALLEL_UPDATES = 1
@@ -35,7 +36,7 @@ PLATFORMS = [
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: PowerviewConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Hunter Douglas PowerView from a config entry."""
config = entry.data
@@ -99,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerviewConfigEntry) ->
# populate raw shade data into the coordinator for diagnostics
coordinator.data.store_group_data(shade_data)
entry.runtime_data = PowerviewEntryData(
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = PowerviewEntryData(
api=pv_request,
room_data=room_data.processed,
scene_data=scene_data.processed,
@@ -125,6 +126,8 @@ async def async_get_device_info(hub: Hub) -> PowerviewDeviceInfo:
)
async def async_unload_entry(hass: HomeAssistant, entry: PowerviewConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -20,13 +20,15 @@ from homeassistant.components.button import (
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import PowerviewShadeUpdateCoordinator
from .entity import ShadeEntity
from .model import PowerviewConfigEntry, PowerviewDeviceInfo
from .model import PowerviewDeviceInfo, PowerviewEntryData
@dataclass(frozen=True)
@@ -73,11 +75,13 @@ BUTTONS_SHADE: Final = [
async def async_setup_entry(
hass: HomeAssistant,
entry: PowerviewConfigEntry,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the hunter douglas advanced feature buttons."""
pv_entry = entry.runtime_data
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
entities: list[ButtonEntity] = []
for shade in pv_entry.shade_data.values():
room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "")

View File

@@ -25,14 +25,15 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
from .const import STATE_ATTRIBUTE_ROOM_NAME
from .const import DOMAIN, STATE_ATTRIBUTE_ROOM_NAME
from .coordinator import PowerviewShadeUpdateCoordinator
from .entity import ShadeEntity
from .model import PowerviewConfigEntry, PowerviewDeviceInfo
from .model import PowerviewDeviceInfo, PowerviewEntryData
_LOGGER = logging.getLogger(__name__)
@@ -48,13 +49,12 @@ SCAN_INTERVAL = timedelta(minutes=10)
async def async_setup_entry(
hass: HomeAssistant,
entry: PowerviewConfigEntry,
async_add_entities: AddEntitiesCallback,
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the hunter douglas shades."""
pv_entry = entry.runtime_data
coordinator = pv_entry.coordinator
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
coordinator: PowerviewShadeUpdateCoordinator = pv_entry.coordinator
async def _async_initial_refresh() -> None:
"""Force position refresh shortly after adding.

View File

@@ -3,18 +3,20 @@
from __future__ import annotations
from dataclasses import asdict
import logging
from typing import Any
import attr
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_CONFIGURATION_URL, CONF_HOST
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntry
from .const import REDACT_HUB_ADDRESS, REDACT_MAC_ADDRESS, REDACT_SERIAL_NUMBER
from .model import PowerviewConfigEntry
from .const import DOMAIN, REDACT_HUB_ADDRESS, REDACT_MAC_ADDRESS, REDACT_SERIAL_NUMBER
from .model import PowerviewEntryData
REDACT_CONFIG = {
CONF_HOST,
@@ -24,9 +26,11 @@ REDACT_CONFIG = {
ATTR_CONFIGURATION_URL,
}
_LOGGER = logging.getLogger(__name__)
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: PowerviewConfigEntry
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data = _async_get_diagnostics(hass, entry)
@@ -43,7 +47,7 @@ async def async_get_config_entry_diagnostics(
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: PowerviewConfigEntry, device: DeviceEntry
hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device entry."""
data = _async_get_diagnostics(hass, entry)
@@ -61,10 +65,10 @@ async def async_get_device_diagnostics(
@callback
def _async_get_diagnostics(
hass: HomeAssistant,
entry: PowerviewConfigEntry,
entry: ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
pv_entry = entry.runtime_data
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
shade_data = pv_entry.coordinator.data.get_all_raw_data()
hub_info = async_redact_data(asdict(pv_entry.device_info), REDACT_CONFIG)
return {"hub_info": hub_info, "shade_data": shade_data}

View File

@@ -9,12 +9,8 @@ from aiopvapi.resources.room import Room
from aiopvapi.resources.scene import Scene
from aiopvapi.resources.shade import BaseShade
from homeassistant.config_entries import ConfigEntry
from .coordinator import PowerviewShadeUpdateCoordinator
type PowerviewConfigEntry = ConfigEntry[PowerviewEntryData]
@dataclass
class PowerviewEntryData:

View File

@@ -2,6 +2,7 @@
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Final
from aiopvapi.helpers.constants import ATTR_NAME, MOTION_VELOCITY
@@ -12,13 +13,17 @@ from homeassistant.components.number import (
NumberMode,
RestoreNumber,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import PowerviewShadeUpdateCoordinator
from .entity import ShadeEntity
from .model import PowerviewConfigEntry, PowerviewDeviceInfo
from .model import PowerviewDeviceInfo, PowerviewEntryData
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
@@ -52,12 +57,12 @@ NUMBERS: Final = (
async def async_setup_entry(
hass: HomeAssistant,
entry: PowerviewConfigEntry,
async_add_entities: AddEntitiesCallback,
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the hunter douglas number entities."""
pv_entry = entry.runtime_data
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
entities: list[PowerViewNumber] = []
for shade in pv_entry.shade_data.values():
room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "")

View File

@@ -9,13 +9,14 @@ from aiopvapi.helpers.constants import ATTR_NAME
from aiopvapi.resources.scene import Scene as PvScene
from homeassistant.components.scene import Scene
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import STATE_ATTRIBUTE_ROOM_NAME
from .const import DOMAIN, STATE_ATTRIBUTE_ROOM_NAME
from .coordinator import PowerviewShadeUpdateCoordinator
from .entity import HDEntity
from .model import PowerviewConfigEntry, PowerviewDeviceInfo
from .model import PowerviewDeviceInfo, PowerviewEntryData
_LOGGER = logging.getLogger(__name__)
@@ -23,12 +24,12 @@ RESYNC_DELAY = 60
async def async_setup_entry(
hass: HomeAssistant,
entry: PowerviewConfigEntry,
async_add_entities: AddEntitiesCallback,
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up powerview scene entries."""
pv_entry = entry.runtime_data
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
pvscenes: list[PowerViewScene] = []
for scene in pv_entry.scene_data.values():
room_name = getattr(pv_entry.room_data.get(scene.room_id), ATTR_NAME, "")

View File

@@ -4,19 +4,24 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
import logging
from typing import Any, Final
from aiopvapi.helpers.constants import ATTR_NAME, FUNCTION_SET_POWER
from aiopvapi.resources.shade import BaseShade
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import PowerviewShadeUpdateCoordinator
from .entity import ShadeEntity
from .model import PowerviewConfigEntry, PowerviewDeviceInfo
from .model import PowerviewDeviceInfo, PowerviewEntryData
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True)
@@ -52,12 +57,12 @@ DROPDOWNS: Final = [
async def async_setup_entry(
hass: HomeAssistant,
entry: PowerviewConfigEntry,
async_add_entities: AddEntitiesCallback,
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the hunter douglas select entities."""
pv_entry = entry.runtime_data
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
entities: list[PowerViewSelect] = []
for shade in pv_entry.shade_data.values():
if not shade.has_battery_info():

View File

@@ -13,13 +13,15 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import PowerviewShadeUpdateCoordinator
from .entity import ShadeEntity
from .model import PowerviewConfigEntry, PowerviewDeviceInfo
from .model import PowerviewDeviceInfo, PowerviewEntryData
@dataclass(frozen=True)
@@ -77,12 +79,12 @@ SENSORS: Final = [
async def async_setup_entry(
hass: HomeAssistant,
entry: PowerviewConfigEntry,
async_add_entities: AddEntitiesCallback,
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the hunter douglas sensor entities."""
pv_entry = entry.runtime_data
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
entities: list[PowerViewSensor] = []
for shade in pv_entry.shade_data.values():
room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "")

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from dataclasses import fields
import logging
from typing import Any
from aiopvapi.resources.model import PowerviewData
@@ -10,6 +11,8 @@ from aiopvapi.resources.shade import BaseShade, ShadePosition
from .util import async_map_data_by_id
_LOGGER = logging.getLogger(__name__)
POSITION_FIELDS = [field for field in fields(ShadePosition) if field.name != "velocity"]

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/incomfort",
"iot_class": "local_polling",
"loggers": ["incomfortclient"],
"requirements": ["incomfort-client==0.6.3-1"]
"requirements": ["incomfort-client==0.6.3"]
}

View File

@@ -121,6 +121,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Remove all input booleans and load new ones from config."""
conf = await component.async_prepare_reload(skip_reset=True)
if conf is None:
return
await yaml_collection.async_load(
[
{CONF_ID: id_, **(conf or {})}

View File

@@ -106,6 +106,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Remove all input buttons and load new ones from config."""
conf = await component.async_prepare_reload(skip_reset=True)
if conf is None:
return
await yaml_collection.async_load(
[
{CONF_ID: id_, **(conf or {})}

View File

@@ -159,6 +159,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Reload yaml entities."""
conf = await component.async_prepare_reload(skip_reset=True)
if conf is None:
conf = {DOMAIN: {}}
await yaml_collection.async_load(
[{CONF_ID: id_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()]
)

View File

@@ -137,6 +137,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Reload yaml entities."""
conf = await component.async_prepare_reload(skip_reset=True)
if conf is None:
conf = {DOMAIN: {}}
await yaml_collection.async_load(
[{CONF_ID: id_, **conf} for id_, conf in conf.get(DOMAIN, {}).items()]
)

View File

@@ -167,6 +167,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Reload yaml entities."""
conf = await component.async_prepare_reload(skip_reset=True)
if conf is None:
conf = {DOMAIN: {}}
await yaml_collection.async_load(
[{CONF_ID: id_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()]
)

Some files were not shown because too many files have changed in this diff Show More