Merge branch 'dev' into epenet-20250602-1736

This commit is contained in:
epenet 2025-06-03 11:03:21 +02:00 committed by GitHub
commit 4996d5c3cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1003 additions and 497 deletions

View File

@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant
from .const import CONNECTION_TYPE, LOCAL from .const import CONNECTION_TYPE, LOCAL
from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator
PLATFORMS = [Platform.CLIMATE] PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:

View File

@ -41,7 +41,30 @@ class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
async def _async_update_data(self) -> dict[str, dict[str, Any]]: async def _async_update_data(self) -> dict[str, dict[str, Any]]:
"""Fetch data from the Adax.""" """Fetch data from the Adax."""
rooms = await self.adax_data_handler.get_rooms() or [] try:
if hasattr(self.adax_data_handler, "fetch_rooms_info"):
rooms = await self.adax_data_handler.fetch_rooms_info() or []
_LOGGER.debug("fetch_rooms_info returned: %s", rooms)
else:
_LOGGER.debug("fetch_rooms_info method not available, using get_rooms")
rooms = []
if not rooms:
_LOGGER.debug(
"No rooms from fetch_rooms_info, trying get_rooms as fallback"
)
rooms = await self.adax_data_handler.get_rooms() or []
_LOGGER.debug("get_rooms fallback returned: %s", rooms)
if not rooms:
raise UpdateFailed("No rooms available from Adax API")
except OSError as e:
raise UpdateFailed(f"Error communicating with API: {e}") from e
for room in rooms:
room["energyWh"] = int(room.get("energyWh", 0))
return {r["id"]: r for r in rooms} return {r["id"]: r for r in rooms}

View File

@ -0,0 +1,77 @@
"""Support for Adax energy sensors."""
from __future__ import annotations
from typing import cast
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import UnitOfEnergy
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AdaxConfigEntry
from .const import CONNECTION_TYPE, DOMAIN, LOCAL
from .coordinator import AdaxCloudCoordinator
async def async_setup_entry(
hass: HomeAssistant,
entry: AdaxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Adax energy sensors with config flow."""
if entry.data.get(CONNECTION_TYPE) != LOCAL:
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
# Create individual energy sensors for each device
async_add_entities(
AdaxEnergySensor(cloud_coordinator, device_id)
for device_id in cloud_coordinator.data
)
class AdaxEnergySensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
"""Representation of an Adax energy sensor."""
_attr_has_entity_name = True
_attr_translation_key = "energy"
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
_attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
_attr_suggested_display_precision = 3
def __init__(
self,
coordinator: AdaxCloudCoordinator,
device_id: str,
) -> None:
"""Initialize the energy sensor."""
super().__init__(coordinator)
self._device_id = device_id
room = coordinator.data[device_id]
self._attr_unique_id = f"{room['homeId']}_{device_id}_energy"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=room["name"],
manufacturer="Adax",
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available and "energyWh" in self.coordinator.data[self._device_id]
)
@property
def native_value(self) -> int:
"""Return the native value of the sensor."""
return int(self.coordinator.data[self._device_id]["energyWh"])

View File

@ -21,7 +21,7 @@ from .const import (
HMIPC_NAME, HMIPC_NAME,
) )
from .hap import HomematicIPConfigEntry, HomematicipHAP from .hap import HomematicIPConfigEntry, HomematicipHAP
from .services import async_setup_services, async_unload_services from .services import async_setup_services
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
@ -116,8 +116,6 @@ async def async_unload_entry(
assert hap.reset_connection_listener is not None assert hap.reset_connection_listener is not None
hap.reset_connection_listener() hap.reset_connection_listener()
await async_unload_services(hass)
return await hap.async_reset() return await hap.async_reset()

View File

@ -123,32 +123,29 @@ SCHEMA_SET_HOME_COOLING_MODE = vol.Schema(
async def async_setup_services(hass: HomeAssistant) -> None: async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the HomematicIP Cloud services.""" """Set up the HomematicIP Cloud services."""
if hass.services.async_services_for_domain(DOMAIN): @verify_domain_entity_control(hass, DOMAIN)
return
@verify_domain_entity_control(DOMAIN)
async def async_call_hmipc_service(service: ServiceCall) -> None: async def async_call_hmipc_service(service: ServiceCall) -> None:
"""Call correct HomematicIP Cloud service.""" """Call correct HomematicIP Cloud service."""
service_name = service.service service_name = service.service
if service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION: if service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION:
await _async_activate_eco_mode_with_duration(hass, service) await _async_activate_eco_mode_with_duration(service)
elif service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD: elif service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD:
await _async_activate_eco_mode_with_period(hass, service) await _async_activate_eco_mode_with_period(service)
elif service_name == SERVICE_ACTIVATE_VACATION: elif service_name == SERVICE_ACTIVATE_VACATION:
await _async_activate_vacation(hass, service) await _async_activate_vacation(service)
elif service_name == SERVICE_DEACTIVATE_ECO_MODE: elif service_name == SERVICE_DEACTIVATE_ECO_MODE:
await _async_deactivate_eco_mode(hass, service) await _async_deactivate_eco_mode(service)
elif service_name == SERVICE_DEACTIVATE_VACATION: elif service_name == SERVICE_DEACTIVATE_VACATION:
await _async_deactivate_vacation(hass, service) await _async_deactivate_vacation(service)
elif service_name == SERVICE_DUMP_HAP_CONFIG: elif service_name == SERVICE_DUMP_HAP_CONFIG:
await _async_dump_hap_config(hass, service) await _async_dump_hap_config(service)
elif service_name == SERVICE_RESET_ENERGY_COUNTER: elif service_name == SERVICE_RESET_ENERGY_COUNTER:
await _async_reset_energy_counter(hass, service) await _async_reset_energy_counter(service)
elif service_name == SERVICE_SET_ACTIVE_CLIMATE_PROFILE: elif service_name == SERVICE_SET_ACTIVE_CLIMATE_PROFILE:
await _set_active_climate_profile(hass, service) await _set_active_climate_profile(service)
elif service_name == SERVICE_SET_HOME_COOLING_MODE: elif service_name == SERVICE_SET_HOME_COOLING_MODE:
await _async_set_home_cooling_mode(hass, service) await _async_set_home_cooling_mode(service)
hass.services.async_register( hass.services.async_register(
domain=DOMAIN, domain=DOMAIN,
@ -217,90 +214,75 @@ async def async_setup_services(hass: HomeAssistant) -> None:
) )
async def async_unload_services(hass: HomeAssistant): async def _async_activate_eco_mode_with_duration(service: ServiceCall) -> None:
"""Unload HomematicIP Cloud services."""
if hass.config_entries.async_loaded_entries(DOMAIN):
return
for hmipc_service in HMIPC_SERVICES:
hass.services.async_remove(domain=DOMAIN, service=hmipc_service)
async def _async_activate_eco_mode_with_duration(
hass: HomeAssistant, service: ServiceCall
) -> None:
"""Service to activate eco mode with duration.""" """Service to activate eco mode with duration."""
duration = service.data[ATTR_DURATION] duration = service.data[ATTR_DURATION]
if hapid := service.data.get(ATTR_ACCESSPOINT_ID): if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(hass, hapid): if home := _get_home(service.hass, hapid):
await home.activate_absence_with_duration_async(duration) await home.activate_absence_with_duration_async(duration)
else: else:
entry: HomematicIPConfigEntry entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN): for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.activate_absence_with_duration_async(duration) await entry.runtime_data.home.activate_absence_with_duration_async(duration)
async def _async_activate_eco_mode_with_period( async def _async_activate_eco_mode_with_period(service: ServiceCall) -> None:
hass: HomeAssistant, service: ServiceCall
) -> None:
"""Service to activate eco mode with period.""" """Service to activate eco mode with period."""
endtime = service.data[ATTR_ENDTIME] endtime = service.data[ATTR_ENDTIME]
if hapid := service.data.get(ATTR_ACCESSPOINT_ID): if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(hass, hapid): if home := _get_home(service.hass, hapid):
await home.activate_absence_with_period_async(endtime) await home.activate_absence_with_period_async(endtime)
else: else:
entry: HomematicIPConfigEntry entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN): for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.activate_absence_with_period_async(endtime) await entry.runtime_data.home.activate_absence_with_period_async(endtime)
async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> None: async def _async_activate_vacation(service: ServiceCall) -> None:
"""Service to activate vacation.""" """Service to activate vacation."""
endtime = service.data[ATTR_ENDTIME] endtime = service.data[ATTR_ENDTIME]
temperature = service.data[ATTR_TEMPERATURE] temperature = service.data[ATTR_TEMPERATURE]
if hapid := service.data.get(ATTR_ACCESSPOINT_ID): if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(hass, hapid): if home := _get_home(service.hass, hapid):
await home.activate_vacation_async(endtime, temperature) await home.activate_vacation_async(endtime, temperature)
else: else:
entry: HomematicIPConfigEntry entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN): for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.activate_vacation_async(endtime, temperature) await entry.runtime_data.home.activate_vacation_async(endtime, temperature)
async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None: async def _async_deactivate_eco_mode(service: ServiceCall) -> None:
"""Service to deactivate eco mode.""" """Service to deactivate eco mode."""
if hapid := service.data.get(ATTR_ACCESSPOINT_ID): if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(hass, hapid): if home := _get_home(service.hass, hapid):
await home.deactivate_absence_async() await home.deactivate_absence_async()
else: else:
entry: HomematicIPConfigEntry entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN): for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.deactivate_absence_async() await entry.runtime_data.home.deactivate_absence_async()
async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None: async def _async_deactivate_vacation(service: ServiceCall) -> None:
"""Service to deactivate vacation.""" """Service to deactivate vacation."""
if hapid := service.data.get(ATTR_ACCESSPOINT_ID): if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(hass, hapid): if home := _get_home(service.hass, hapid):
await home.deactivate_vacation_async() await home.deactivate_vacation_async()
else: else:
entry: HomematicIPConfigEntry entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN): for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.deactivate_vacation_async() await entry.runtime_data.home.deactivate_vacation_async()
async def _set_active_climate_profile( async def _set_active_climate_profile(service: ServiceCall) -> None:
hass: HomeAssistant, service: ServiceCall
) -> None:
"""Service to set the active climate profile.""" """Service to set the active climate profile."""
entity_id_list = service.data[ATTR_ENTITY_ID] entity_id_list = service.data[ATTR_ENTITY_ID]
climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1
entry: HomematicIPConfigEntry entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN): for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
if entity_id_list != "all": if entity_id_list != "all":
for entity_id in entity_id_list: for entity_id in entity_id_list:
group = entry.runtime_data.hmip_device_by_entity_id.get(entity_id) group = entry.runtime_data.hmip_device_by_entity_id.get(entity_id)
@ -312,16 +294,16 @@ async def _set_active_climate_profile(
await group.set_active_profile_async(climate_profile_index) await group.set_active_profile_async(climate_profile_index)
async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> None: async def _async_dump_hap_config(service: ServiceCall) -> None:
"""Service to dump the configuration of a Homematic IP Access Point.""" """Service to dump the configuration of a Homematic IP Access Point."""
config_path: str = ( config_path: str = (
service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir service.data.get(ATTR_CONFIG_OUTPUT_PATH) or service.hass.config.config_dir
) )
config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX]
anonymize = service.data[ATTR_ANONYMIZE] anonymize = service.data[ATTR_ANONYMIZE]
entry: HomematicIPConfigEntry entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN): for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
hap_sgtin = entry.unique_id hap_sgtin = entry.unique_id
assert hap_sgtin is not None assert hap_sgtin is not None
@ -338,12 +320,12 @@ async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> N
config_file.write_text(json_state, encoding="utf8") config_file.write_text(json_state, encoding="utf8")
async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall): async def _async_reset_energy_counter(service: ServiceCall):
"""Service to reset the energy counter.""" """Service to reset the energy counter."""
entity_id_list = service.data[ATTR_ENTITY_ID] entity_id_list = service.data[ATTR_ENTITY_ID]
entry: HomematicIPConfigEntry entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN): for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
if entity_id_list != "all": if entity_id_list != "all":
for entity_id in entity_id_list: for entity_id in entity_id_list:
device = entry.runtime_data.hmip_device_by_entity_id.get(entity_id) device = entry.runtime_data.hmip_device_by_entity_id.get(entity_id)
@ -355,16 +337,16 @@ async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall)
await device.reset_energy_counter_async() await device.reset_energy_counter_async()
async def _async_set_home_cooling_mode(hass: HomeAssistant, service: ServiceCall): async def _async_set_home_cooling_mode(service: ServiceCall):
"""Service to set the cooling mode.""" """Service to set the cooling mode."""
cooling = service.data[ATTR_COOLING] cooling = service.data[ATTR_COOLING]
if hapid := service.data.get(ATTR_ACCESSPOINT_ID): if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(hass, hapid): if home := _get_home(service.hass, hapid):
await home.set_cooling_async(cooling) await home.set_cooling_async(cooling)
else: else:
entry: HomematicIPConfigEntry entry: HomematicIPConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN): for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.set_cooling_async(cooling) await entry.runtime_data.home.set_cooling_async(cooling)

View File

@ -6,19 +6,32 @@ from typing import Any
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType
from .account import IcloudAccount, IcloudConfigEntry from .account import IcloudAccount, IcloudConfigEntry
from .const import ( from .const import (
CONF_GPS_ACCURACY_THRESHOLD, CONF_GPS_ACCURACY_THRESHOLD,
CONF_MAX_INTERVAL, CONF_MAX_INTERVAL,
CONF_WITH_FAMILY, CONF_WITH_FAMILY,
DOMAIN,
PLATFORMS, PLATFORMS,
STORAGE_KEY, STORAGE_KEY,
STORAGE_VERSION, STORAGE_VERSION,
) )
from .services import register_services from .services import register_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up iCloud integration."""
register_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bool:
"""Set up an iCloud account from a config entry.""" """Set up an iCloud account from a config entry."""
@ -51,8 +64,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bo
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
register_services(hass)
return True return True

View File

@ -25,9 +25,9 @@ from .const import (
DOMAIN, DOMAIN,
INSTEON_PLATFORMS, INSTEON_PLATFORMS,
) )
from .services import async_register_services
from .utils import ( from .utils import (
add_insteon_events, add_insteon_events,
async_register_services,
get_device_platforms, get_device_platforms,
register_new_device_callback, register_new_device_callback,
) )

View File

@ -0,0 +1,291 @@
"""Utilities used by insteon component."""
from __future__ import annotations
import asyncio
import logging
from pyinsteon import devices
from pyinsteon.address import Address
from pyinsteon.managers.link_manager import (
async_enter_linking_mode,
async_enter_unlinking_mode,
)
from pyinsteon.managers.scene_manager import (
async_trigger_scene_off,
async_trigger_scene_on,
)
from pyinsteon.managers.x10_manager import (
async_x10_all_lights_off,
async_x10_all_lights_on,
async_x10_all_units_off,
)
from pyinsteon.x10_address import create as create_x10_address
from homeassistant.const import (
CONF_ADDRESS,
CONF_ENTITY_ID,
CONF_PLATFORM,
ENTITY_MATCH_ALL,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
dispatcher_send,
)
from .const import (
CONF_CAT,
CONF_DIM_STEPS,
CONF_HOUSECODE,
CONF_SUBCAT,
CONF_UNITCODE,
DOMAIN,
SIGNAL_ADD_DEFAULT_LINKS,
SIGNAL_ADD_DEVICE_OVERRIDE,
SIGNAL_ADD_X10_DEVICE,
SIGNAL_LOAD_ALDB,
SIGNAL_PRINT_ALDB,
SIGNAL_REMOVE_DEVICE_OVERRIDE,
SIGNAL_REMOVE_ENTITY,
SIGNAL_REMOVE_HA_DEVICE,
SIGNAL_REMOVE_INSTEON_DEVICE,
SIGNAL_REMOVE_X10_DEVICE,
SIGNAL_SAVE_DEVICES,
SRV_ADD_ALL_LINK,
SRV_ADD_DEFAULT_LINKS,
SRV_ALL_LINK_GROUP,
SRV_ALL_LINK_MODE,
SRV_CONTROLLER,
SRV_DEL_ALL_LINK,
SRV_HOUSECODE,
SRV_LOAD_ALDB,
SRV_LOAD_DB_RELOAD,
SRV_PRINT_ALDB,
SRV_PRINT_IM_ALDB,
SRV_SCENE_OFF,
SRV_SCENE_ON,
SRV_X10_ALL_LIGHTS_OFF,
SRV_X10_ALL_LIGHTS_ON,
SRV_X10_ALL_UNITS_OFF,
)
from .schemas import (
ADD_ALL_LINK_SCHEMA,
ADD_DEFAULT_LINKS_SCHEMA,
DEL_ALL_LINK_SCHEMA,
LOAD_ALDB_SCHEMA,
PRINT_ALDB_SCHEMA,
TRIGGER_SCENE_SCHEMA,
X10_HOUSECODE_SCHEMA,
)
from .utils import print_aldb_to_log
_LOGGER = logging.getLogger(__name__)
@callback
def async_register_services(hass: HomeAssistant) -> None: # noqa: C901
"""Register services used by insteon component."""
save_lock = asyncio.Lock()
async def async_srv_add_all_link(service: ServiceCall) -> None:
"""Add an INSTEON All-Link between two devices."""
group = service.data[SRV_ALL_LINK_GROUP]
mode = service.data[SRV_ALL_LINK_MODE]
link_mode = mode.lower() == SRV_CONTROLLER
await async_enter_linking_mode(link_mode, group)
async def async_srv_del_all_link(service: ServiceCall) -> None:
"""Delete an INSTEON All-Link between two devices."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_enter_unlinking_mode(group)
async def async_srv_load_aldb(service: ServiceCall) -> None:
"""Load the device All-Link database."""
entity_id = service.data[CONF_ENTITY_ID]
reload = service.data[SRV_LOAD_DB_RELOAD]
if entity_id.lower() == ENTITY_MATCH_ALL:
await async_srv_load_aldb_all(reload)
else:
signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
async_dispatcher_send(hass, signal, reload)
async def async_srv_load_aldb_all(reload):
"""Load the All-Link database for all devices."""
# Cannot be done concurrently due to issues with the underlying protocol.
for address in devices:
device = devices[address]
if device != devices.modem and device.cat != 0x03:
await device.aldb.async_load(refresh=reload)
await async_srv_save_devices()
async def async_srv_save_devices():
"""Write the Insteon device configuration to file."""
async with save_lock:
_LOGGER.debug("Saving Insteon devices")
await devices.async_save(hass.config.config_dir)
def print_aldb(service: ServiceCall) -> None:
"""Print the All-Link Database for a device."""
# For now this sends logs to the log file.
# Future direction is to create an INSTEON control panel.
entity_id = service.data[CONF_ENTITY_ID]
signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
dispatcher_send(hass, signal)
def print_im_aldb(service: ServiceCall) -> None:
"""Print the All-Link Database for a device."""
# For now this sends logs to the log file.
# Future direction is to create an INSTEON control panel.
print_aldb_to_log(devices.modem.aldb)
async def async_srv_x10_all_units_off(service: ServiceCall) -> None:
"""Send the X10 All Units Off command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_units_off(housecode)
async def async_srv_x10_all_lights_off(service: ServiceCall) -> None:
"""Send the X10 All Lights Off command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_lights_off(housecode)
async def async_srv_x10_all_lights_on(service: ServiceCall) -> None:
"""Send the X10 All Lights On command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_lights_on(housecode)
async def async_srv_scene_on(service: ServiceCall) -> None:
"""Trigger an INSTEON scene ON."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_trigger_scene_on(group)
async def async_srv_scene_off(service: ServiceCall) -> None:
"""Trigger an INSTEON scene ON."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_trigger_scene_off(group)
@callback
def async_add_default_links(service: ServiceCall) -> None:
"""Add the default All-Link entries to a device."""
entity_id = service.data[CONF_ENTITY_ID]
signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}"
async_dispatcher_send(hass, signal)
async def async_add_device_override(override):
"""Remove an Insten device and associated entities."""
address = Address(override[CONF_ADDRESS])
await async_remove_ha_device(address)
devices.set_id(address, override[CONF_CAT], override[CONF_SUBCAT], 0)
await async_srv_save_devices()
async def async_remove_device_override(address):
"""Remove an Insten device and associated entities."""
address = Address(address)
await async_remove_ha_device(address)
devices.set_id(address, None, None, None)
await devices.async_identify_device(address)
await async_srv_save_devices()
@callback
def async_add_x10_device(x10_config):
"""Add X10 device."""
housecode = x10_config[CONF_HOUSECODE]
unitcode = x10_config[CONF_UNITCODE]
platform = x10_config[CONF_PLATFORM]
steps = x10_config.get(CONF_DIM_STEPS, 22)
x10_type = "on_off"
if platform == "light":
x10_type = "dimmable"
elif platform == "binary_sensor":
x10_type = "sensor"
_LOGGER.debug(
"Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type
)
# This must be run in the event loop
devices.add_x10_device(housecode, unitcode, x10_type, steps)
async def async_remove_x10_device(housecode, unitcode):
"""Remove an X10 device and associated entities."""
address = create_x10_address(housecode, unitcode)
devices.pop(address)
await async_remove_ha_device(address)
async def async_remove_ha_device(address: Address, remove_all_refs: bool = False):
"""Remove the device and all entities from hass."""
signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}"
async_dispatcher_send(hass, signal)
dev_registry = dr.async_get(hass)
device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))})
if device:
dev_registry.async_remove_device(device.id)
async def async_remove_insteon_device(
address: Address, remove_all_refs: bool = False
):
"""Remove the underlying Insteon device from the network."""
await devices.async_remove_device(
address=address, force=False, remove_all_refs=remove_all_refs
)
await async_srv_save_devices()
hass.services.async_register(
DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_DEL_ALL_LINK, async_srv_del_all_link, schema=DEL_ALL_LINK_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_LOAD_ALDB, async_srv_load_aldb, schema=LOAD_ALDB_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
)
hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_UNITS_OFF,
async_srv_x10_all_units_off,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_LIGHTS_OFF,
async_srv_x10_all_lights_off,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_LIGHTS_ON,
async_srv_x10_all_lights_on,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN, SRV_SCENE_ON, async_srv_scene_on, schema=TRIGGER_SCENE_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA
)
hass.services.async_register(
DOMAIN,
SRV_ADD_DEFAULT_LINKS,
async_add_default_links,
schema=ADD_DEFAULT_LINKS_SCHEMA,
)
async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices)
async_dispatcher_connect(
hass, SIGNAL_ADD_DEVICE_OVERRIDE, async_add_device_override
)
async_dispatcher_connect(
hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, async_remove_device_override
)
async_dispatcher_connect(hass, SIGNAL_ADD_X10_DEVICE, async_add_x10_device)
async_dispatcher_connect(hass, SIGNAL_REMOVE_X10_DEVICE, async_remove_x10_device)
async_dispatcher_connect(hass, SIGNAL_REMOVE_HA_DEVICE, async_remove_ha_device)
async_dispatcher_connect(
hass, SIGNAL_REMOVE_INSTEON_DEVICE, async_remove_insteon_device
)
_LOGGER.debug("Insteon Services registered")

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Callable from collections.abc import Callable
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@ -12,90 +11,25 @@ from pyinsteon.address import Address
from pyinsteon.constants import ALDBStatus, DeviceAction from pyinsteon.constants import ALDBStatus, DeviceAction
from pyinsteon.device_types.device_base import Device from pyinsteon.device_types.device_base import Device
from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT, Event from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT, Event
from pyinsteon.managers.link_manager import (
async_enter_linking_mode,
async_enter_unlinking_mode,
)
from pyinsteon.managers.scene_manager import (
async_trigger_scene_off,
async_trigger_scene_on,
)
from pyinsteon.managers.x10_manager import (
async_x10_all_lights_off,
async_x10_all_lights_on,
async_x10_all_units_off,
)
from pyinsteon.x10_address import create as create_x10_address
from serial.tools import list_ports from serial.tools import list_ports
from homeassistant.components import usb from homeassistant.components import usb
from homeassistant.const import ( from homeassistant.const import CONF_ADDRESS, Platform
CONF_ADDRESS, from homeassistant.core import HomeAssistant, callback
CONF_ENTITY_ID,
CONF_PLATFORM,
ENTITY_MATCH_ALL,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import dispatcher_send
async_dispatcher_connect,
async_dispatcher_send,
dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ( from .const import (
CONF_CAT,
CONF_DIM_STEPS,
CONF_HOUSECODE,
CONF_SUBCAT,
CONF_UNITCODE,
DOMAIN, DOMAIN,
EVENT_CONF_BUTTON, EVENT_CONF_BUTTON,
EVENT_GROUP_OFF, EVENT_GROUP_OFF,
EVENT_GROUP_OFF_FAST, EVENT_GROUP_OFF_FAST,
EVENT_GROUP_ON, EVENT_GROUP_ON,
EVENT_GROUP_ON_FAST, EVENT_GROUP_ON_FAST,
SIGNAL_ADD_DEFAULT_LINKS,
SIGNAL_ADD_DEVICE_OVERRIDE,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ADD_X10_DEVICE,
SIGNAL_LOAD_ALDB,
SIGNAL_PRINT_ALDB,
SIGNAL_REMOVE_DEVICE_OVERRIDE,
SIGNAL_REMOVE_ENTITY,
SIGNAL_REMOVE_HA_DEVICE,
SIGNAL_REMOVE_INSTEON_DEVICE,
SIGNAL_REMOVE_X10_DEVICE,
SIGNAL_SAVE_DEVICES,
SRV_ADD_ALL_LINK,
SRV_ADD_DEFAULT_LINKS,
SRV_ALL_LINK_GROUP,
SRV_ALL_LINK_MODE,
SRV_CONTROLLER,
SRV_DEL_ALL_LINK,
SRV_HOUSECODE,
SRV_LOAD_ALDB,
SRV_LOAD_DB_RELOAD,
SRV_PRINT_ALDB,
SRV_PRINT_IM_ALDB,
SRV_SCENE_OFF,
SRV_SCENE_ON,
SRV_X10_ALL_LIGHTS_OFF,
SRV_X10_ALL_LIGHTS_ON,
SRV_X10_ALL_UNITS_OFF,
) )
from .ipdb import get_device_platform_groups, get_device_platforms from .ipdb import get_device_platform_groups, get_device_platforms
from .schemas import (
ADD_ALL_LINK_SCHEMA,
ADD_DEFAULT_LINKS_SCHEMA,
DEL_ALL_LINK_SCHEMA,
LOAD_ALDB_SCHEMA,
PRINT_ALDB_SCHEMA,
TRIGGER_SCENE_SCHEMA,
X10_HOUSECODE_SCHEMA,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from .entity import InsteonEntity from .entity import InsteonEntity
@ -154,7 +88,7 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
_register_event(event, async_fire_insteon_event) _register_event(event, async_fire_insteon_event)
def register_new_device_callback(hass): def register_new_device_callback(hass: HomeAssistant) -> None:
"""Register callback for new Insteon device.""" """Register callback for new Insteon device."""
@callback @callback
@ -180,212 +114,6 @@ def register_new_device_callback(hass):
devices.subscribe(async_new_insteon_device, force_strong_ref=True) devices.subscribe(async_new_insteon_device, force_strong_ref=True)
@callback
def async_register_services(hass): # noqa: C901
"""Register services used by insteon component."""
save_lock = asyncio.Lock()
async def async_srv_add_all_link(service: ServiceCall) -> None:
"""Add an INSTEON All-Link between two devices."""
group = service.data[SRV_ALL_LINK_GROUP]
mode = service.data[SRV_ALL_LINK_MODE]
link_mode = mode.lower() == SRV_CONTROLLER
await async_enter_linking_mode(link_mode, group)
async def async_srv_del_all_link(service: ServiceCall) -> None:
"""Delete an INSTEON All-Link between two devices."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_enter_unlinking_mode(group)
async def async_srv_load_aldb(service: ServiceCall) -> None:
"""Load the device All-Link database."""
entity_id = service.data[CONF_ENTITY_ID]
reload = service.data[SRV_LOAD_DB_RELOAD]
if entity_id.lower() == ENTITY_MATCH_ALL:
await async_srv_load_aldb_all(reload)
else:
signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
async_dispatcher_send(hass, signal, reload)
async def async_srv_load_aldb_all(reload):
"""Load the All-Link database for all devices."""
# Cannot be done concurrently due to issues with the underlying protocol.
for address in devices:
device = devices[address]
if device != devices.modem and device.cat != 0x03:
await device.aldb.async_load(refresh=reload)
await async_srv_save_devices()
async def async_srv_save_devices():
"""Write the Insteon device configuration to file."""
async with save_lock:
_LOGGER.debug("Saving Insteon devices")
await devices.async_save(hass.config.config_dir)
def print_aldb(service: ServiceCall) -> None:
"""Print the All-Link Database for a device."""
# For now this sends logs to the log file.
# Future direction is to create an INSTEON control panel.
entity_id = service.data[CONF_ENTITY_ID]
signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
dispatcher_send(hass, signal)
def print_im_aldb(service: ServiceCall) -> None:
"""Print the All-Link Database for a device."""
# For now this sends logs to the log file.
# Future direction is to create an INSTEON control panel.
print_aldb_to_log(devices.modem.aldb)
async def async_srv_x10_all_units_off(service: ServiceCall) -> None:
"""Send the X10 All Units Off command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_units_off(housecode)
async def async_srv_x10_all_lights_off(service: ServiceCall) -> None:
"""Send the X10 All Lights Off command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_lights_off(housecode)
async def async_srv_x10_all_lights_on(service: ServiceCall) -> None:
"""Send the X10 All Lights On command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_lights_on(housecode)
async def async_srv_scene_on(service: ServiceCall) -> None:
"""Trigger an INSTEON scene ON."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_trigger_scene_on(group)
async def async_srv_scene_off(service: ServiceCall) -> None:
"""Trigger an INSTEON scene ON."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_trigger_scene_off(group)
@callback
def async_add_default_links(service: ServiceCall) -> None:
"""Add the default All-Link entries to a device."""
entity_id = service.data[CONF_ENTITY_ID]
signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}"
async_dispatcher_send(hass, signal)
async def async_add_device_override(override):
"""Remove an Insten device and associated entities."""
address = Address(override[CONF_ADDRESS])
await async_remove_ha_device(address)
devices.set_id(address, override[CONF_CAT], override[CONF_SUBCAT], 0)
await async_srv_save_devices()
async def async_remove_device_override(address):
"""Remove an Insten device and associated entities."""
address = Address(address)
await async_remove_ha_device(address)
devices.set_id(address, None, None, None)
await devices.async_identify_device(address)
await async_srv_save_devices()
@callback
def async_add_x10_device(x10_config):
"""Add X10 device."""
housecode = x10_config[CONF_HOUSECODE]
unitcode = x10_config[CONF_UNITCODE]
platform = x10_config[CONF_PLATFORM]
steps = x10_config.get(CONF_DIM_STEPS, 22)
x10_type = "on_off"
if platform == "light":
x10_type = "dimmable"
elif platform == "binary_sensor":
x10_type = "sensor"
_LOGGER.debug(
"Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type
)
# This must be run in the event loop
devices.add_x10_device(housecode, unitcode, x10_type, steps)
async def async_remove_x10_device(housecode, unitcode):
"""Remove an X10 device and associated entities."""
address = create_x10_address(housecode, unitcode)
devices.pop(address)
await async_remove_ha_device(address)
async def async_remove_ha_device(address: Address, remove_all_refs: bool = False):
"""Remove the device and all entities from hass."""
signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}"
async_dispatcher_send(hass, signal)
dev_registry = dr.async_get(hass)
device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))})
if device:
dev_registry.async_remove_device(device.id)
async def async_remove_insteon_device(
address: Address, remove_all_refs: bool = False
):
"""Remove the underlying Insteon device from the network."""
await devices.async_remove_device(
address=address, force=False, remove_all_refs=remove_all_refs
)
await async_srv_save_devices()
hass.services.async_register(
DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_DEL_ALL_LINK, async_srv_del_all_link, schema=DEL_ALL_LINK_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_LOAD_ALDB, async_srv_load_aldb, schema=LOAD_ALDB_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
)
hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_UNITS_OFF,
async_srv_x10_all_units_off,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_LIGHTS_OFF,
async_srv_x10_all_lights_off,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_LIGHTS_ON,
async_srv_x10_all_lights_on,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN, SRV_SCENE_ON, async_srv_scene_on, schema=TRIGGER_SCENE_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA
)
hass.services.async_register(
DOMAIN,
SRV_ADD_DEFAULT_LINKS,
async_add_default_links,
schema=ADD_DEFAULT_LINKS_SCHEMA,
)
async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices)
async_dispatcher_connect(
hass, SIGNAL_ADD_DEVICE_OVERRIDE, async_add_device_override
)
async_dispatcher_connect(
hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, async_remove_device_override
)
async_dispatcher_connect(hass, SIGNAL_ADD_X10_DEVICE, async_add_x10_device)
async_dispatcher_connect(hass, SIGNAL_REMOVE_X10_DEVICE, async_remove_x10_device)
async_dispatcher_connect(hass, SIGNAL_REMOVE_HA_DEVICE, async_remove_ha_device)
async_dispatcher_connect(
hass, SIGNAL_REMOVE_INSTEON_DEVICE, async_remove_insteon_device
)
_LOGGER.debug("Insteon Services registered")
def print_aldb_to_log(aldb): def print_aldb_to_log(aldb):
"""Print the All-Link Database to the log file.""" """Print the All-Link Database to the log file."""
logger = logging.getLogger(f"{__name__}.links") logger = logging.getLogger(f"{__name__}.links")

View File

@ -1,30 +1,25 @@
"""The NZBGet integration.""" """The NZBGet integration."""
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import DATA_COORDINATOR, DATA_UNDO_UPDATE_LISTENER, DOMAIN
ATTR_SPEED,
DATA_COORDINATOR,
DATA_UNDO_UPDATE_LISTENER,
DEFAULT_SPEED_LIMIT,
DOMAIN,
SERVICE_PAUSE,
SERVICE_RESUME,
SERVICE_SET_SPEED,
)
from .coordinator import NZBGetDataUpdateCoordinator from .coordinator import NZBGetDataUpdateCoordinator
from .services import async_register_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [Platform.SENSOR, Platform.SWITCH] PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
SPEED_LIMIT_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int} """Set up NZBGet integration."""
)
async_register_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -44,8 +39,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
_async_register_services(hass, coordinator)
return True return True
@ -60,31 +53,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok return unload_ok
def _async_register_services(
hass: HomeAssistant,
coordinator: NZBGetDataUpdateCoordinator,
) -> None:
"""Register integration-level services."""
def pause(call: ServiceCall) -> None:
"""Service call to pause downloads in NZBGet."""
coordinator.nzbget.pausedownload()
def resume(call: ServiceCall) -> None:
"""Service call to resume downloads in NZBGet."""
coordinator.nzbget.resumedownload()
def set_speed(call: ServiceCall) -> None:
"""Service call to rate limit speeds in NZBGet."""
coordinator.nzbget.rate(call.data[ATTR_SPEED])
hass.services.async_register(DOMAIN, SERVICE_PAUSE, pause, schema=vol.Schema({}))
hass.services.async_register(DOMAIN, SERVICE_RESUME, resume, schema=vol.Schema({}))
hass.services.async_register(
DOMAIN, SERVICE_SET_SPEED, set_speed, schema=SPEED_LIMIT_SCHEMA
)
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update.""" """Handle options update."""
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)

View File

@ -0,0 +1,58 @@
"""The NZBGet integration."""
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from .const import (
ATTR_SPEED,
DATA_COORDINATOR,
DEFAULT_SPEED_LIMIT,
DOMAIN,
SERVICE_PAUSE,
SERVICE_RESUME,
SERVICE_SET_SPEED,
)
from .coordinator import NZBGetDataUpdateCoordinator
SPEED_LIMIT_SCHEMA = vol.Schema(
{vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int}
)
def _get_coordinator(call: ServiceCall) -> NZBGetDataUpdateCoordinator:
"""Service call to pause downloads in NZBGet."""
entries = call.hass.config_entries.async_loaded_entries(DOMAIN)
if not entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_config_entry",
)
return call.hass.data[DOMAIN][entries[0].entry_id][DATA_COORDINATOR]
def pause(call: ServiceCall) -> None:
"""Service call to pause downloads in NZBGet."""
_get_coordinator(call).nzbget.pausedownload()
def resume(call: ServiceCall) -> None:
"""Service call to resume downloads in NZBGet."""
_get_coordinator(call).nzbget.resumedownload()
def set_speed(call: ServiceCall) -> None:
"""Service call to rate limit speeds in NZBGet."""
_get_coordinator(call).nzbget.rate(call.data[ATTR_SPEED])
def async_register_services(hass: HomeAssistant) -> None:
"""Register integration-level services."""
hass.services.async_register(DOMAIN, SERVICE_PAUSE, pause, schema=vol.Schema({}))
hass.services.async_register(DOMAIN, SERVICE_RESUME, resume, schema=vol.Schema({}))
hass.services.async_register(
DOMAIN, SERVICE_SET_SPEED, set_speed, schema=SPEED_LIMIT_SCHEMA
)

View File

@ -64,6 +64,11 @@
} }
} }
}, },
"exceptions": {
"invalid_config_entry": {
"message": "Config entry not found or not loaded!"
}
},
"services": { "services": {
"pause": { "pause": {
"name": "[%key:common::action::pause%]", "name": "[%key:common::action::pause%]",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/picnic", "documentation": "https://www.home-assistant.io/integrations/picnic",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["python_picnic_api2"], "loggers": ["python_picnic_api2"],
"requirements": ["python-picnic-api2==1.2.4"] "requirements": ["python-picnic-api2==1.3.1"]
} }

View File

@ -205,7 +205,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
key="charge_state_charging_state", key="charge_state_charging_state",
polling=True, polling=True,
streaming_listener=lambda vehicle, callback: vehicle.listen_DetailedChargeState( streaming_listener=lambda vehicle, callback: vehicle.listen_DetailedChargeState(
lambda value: None if value is None else callback(value.lower()) lambda value: callback(None if value is None else CHARGE_STATES.get(value))
), ),
polling_value_fn=lambda value: CHARGE_STATES.get(str(value)), polling_value_fn=lambda value: CHARGE_STATES.get(str(value)),
options=list(CHARGE_STATES.values()), options=list(CHARGE_STATES.values()),
@ -533,7 +533,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
TeslemetryVehicleSensorEntityDescription( TeslemetryVehicleSensorEntityDescription(
key="bms_state", key="bms_state",
streaming_listener=lambda vehicle, callback: vehicle.listen_BMSState( streaming_listener=lambda vehicle, callback: vehicle.listen_BMSState(
lambda value: None if value is None else callback(BMS_STATES.get(value)) lambda value: callback(None if value is None else BMS_STATES.get(value))
), ),
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options=list(BMS_STATES.values()), options=list(BMS_STATES.values()),

2
requirements_all.txt generated
View File

@ -2486,7 +2486,7 @@ python-otbr-api==2.7.0
python-overseerr==0.7.1 python-overseerr==0.7.1
# homeassistant.components.picnic # homeassistant.components.picnic
python-picnic-api2==1.2.4 python-picnic-api2==1.3.1
# homeassistant.components.rabbitair # homeassistant.components.rabbitair
python-rabbitair==0.0.8 python-rabbitair==0.0.8

View File

@ -2053,7 +2053,7 @@ python-otbr-api==2.7.0
python-overseerr==0.7.1 python-overseerr==0.7.1
# homeassistant.components.picnic # homeassistant.components.picnic
python-picnic-api2==1.2.4 python-picnic-api2==1.3.1
# homeassistant.components.rabbitair # homeassistant.components.rabbitair
python-rabbitair==0.0.8 python-rabbitair==0.0.8

View File

@ -43,6 +43,7 @@ CLOUD_DEVICE_DATA: dict[str, Any] = [
"temperature": 15, "temperature": 15,
"targetTemperature": 20, "targetTemperature": 20,
"heatingEnabled": True, "heatingEnabled": True,
"energyWh": 1500,
} }
] ]
@ -70,9 +71,17 @@ def mock_adax_cloud():
with patch("homeassistant.components.adax.coordinator.Adax") as mock_adax: with patch("homeassistant.components.adax.coordinator.Adax") as mock_adax:
mock_adax_class = mock_adax.return_value mock_adax_class = mock_adax.return_value
mock_adax_class.fetch_rooms_info = AsyncMock()
mock_adax_class.fetch_rooms_info.return_value = CLOUD_DEVICE_DATA
mock_adax_class.get_rooms = AsyncMock() mock_adax_class.get_rooms = AsyncMock()
mock_adax_class.get_rooms.return_value = CLOUD_DEVICE_DATA mock_adax_class.get_rooms.return_value = CLOUD_DEVICE_DATA
mock_adax_class.fetch_energy_info = AsyncMock()
mock_adax_class.fetch_energy_info.return_value = [
{"deviceId": "1", "energyWh": 1500}
]
mock_adax_class.update = AsyncMock() mock_adax_class.update = AsyncMock()
mock_adax_class.update.return_value = None mock_adax_class.update.return_value = None
yield mock_adax_class yield mock_adax_class

View File

@ -0,0 +1,237 @@
# serializer version: 1
# name: test_fallback_to_get_rooms[sensor.room_1_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.room_1_energy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy',
'platform': 'adax',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'energy',
'unique_id': '1_1_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_fallback_to_get_rooms[sensor.room_1_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Room 1 Energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.room_1_energy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_multiple_devices_create_individual_sensors[sensor.room_1_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.room_1_energy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy',
'platform': 'adax',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'energy',
'unique_id': '1_1_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_multiple_devices_create_individual_sensors[sensor.room_1_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Room 1 Energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.room_1_energy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.5',
})
# ---
# name: test_multiple_devices_create_individual_sensors[sensor.room_2_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.room_2_energy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy',
'platform': 'adax',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'energy',
'unique_id': '1_2_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_multiple_devices_create_individual_sensors[sensor.room_2_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Room 2 Energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.room_2_energy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2.5',
})
# ---
# name: test_sensor_cloud[sensor.room_1_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.room_1_energy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy',
'platform': 'adax',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'energy',
'unique_id': '1_1_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_sensor_cloud[sensor.room_1_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Room 1 Energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.room_1_energy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.5',
})
# ---

View File

@ -20,7 +20,7 @@ async def test_climate_cloud(
) -> None: ) -> None:
"""Test states of the (cloud) Climate entity.""" """Test states of the (cloud) Climate entity."""
await setup_integration(hass, mock_cloud_config_entry) await setup_integration(hass, mock_cloud_config_entry)
mock_adax_cloud.get_rooms.assert_called_once() mock_adax_cloud.fetch_rooms_info.assert_called_once()
assert len(hass.states.async_entity_ids(Platform.CLIMATE)) == 1 assert len(hass.states.async_entity_ids(Platform.CLIMATE)) == 1
entity_id = hass.states.async_entity_ids(Platform.CLIMATE)[0] entity_id = hass.states.async_entity_ids(Platform.CLIMATE)[0]
@ -37,7 +37,7 @@ async def test_climate_cloud(
== CLOUD_DEVICE_DATA[0]["temperature"] == CLOUD_DEVICE_DATA[0]["temperature"]
) )
mock_adax_cloud.get_rooms.side_effect = Exception() mock_adax_cloud.fetch_rooms_info.side_effect = Exception()
freezer.tick(SCAN_INTERVAL) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -0,0 +1,121 @@
"""Test Adax sensor entity."""
from unittest.mock import AsyncMock, patch
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_sensor_cloud(
hass: HomeAssistant,
mock_adax_cloud: AsyncMock,
mock_cloud_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test sensor setup for cloud connection."""
with patch("homeassistant.components.adax.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_cloud_config_entry)
# Now we use fetch_rooms_info as primary method
mock_adax_cloud.fetch_rooms_info.assert_called_once()
await snapshot_platform(
hass, entity_registry, snapshot, mock_cloud_config_entry.entry_id
)
async def test_sensor_local_not_created(
hass: HomeAssistant,
mock_adax_local: AsyncMock,
mock_local_config_entry: MockConfigEntry,
) -> None:
"""Test that sensors are not created for local connection."""
with patch("homeassistant.components.adax.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_local_config_entry)
# No sensor entities should be created for local connection
sensor_entities = hass.states.async_entity_ids("sensor")
adax_sensors = [e for e in sensor_entities if "adax" in e or "room" in e]
assert len(adax_sensors) == 0
async def test_multiple_devices_create_individual_sensors(
hass: HomeAssistant,
mock_adax_cloud: AsyncMock,
mock_cloud_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that multiple devices create individual sensors."""
# Mock multiple devices for both fetch_rooms_info and get_rooms (fallback)
multiple_devices_data = [
{
"id": "1",
"homeId": "1",
"name": "Room 1",
"temperature": 15,
"targetTemperature": 20,
"heatingEnabled": True,
"energyWh": 1500,
},
{
"id": "2",
"homeId": "1",
"name": "Room 2",
"temperature": 18,
"targetTemperature": 22,
"heatingEnabled": True,
"energyWh": 2500,
},
]
mock_adax_cloud.fetch_rooms_info.return_value = multiple_devices_data
mock_adax_cloud.get_rooms.return_value = multiple_devices_data
with patch("homeassistant.components.adax.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_cloud_config_entry)
await snapshot_platform(
hass, entity_registry, snapshot, mock_cloud_config_entry.entry_id
)
async def test_fallback_to_get_rooms(
hass: HomeAssistant,
mock_adax_cloud: AsyncMock,
mock_cloud_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test fallback to get_rooms when fetch_rooms_info returns empty list."""
# Mock fetch_rooms_info to return empty list, get_rooms to return data
mock_adax_cloud.fetch_rooms_info.return_value = []
mock_adax_cloud.get_rooms.return_value = [
{
"id": "1",
"homeId": "1",
"name": "Room 1",
"temperature": 15,
"targetTemperature": 20,
"heatingEnabled": True,
"energyWh": 0, # No energy data from get_rooms
}
]
with patch("homeassistant.components.adax.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_cloud_config_entry)
# Should call both methods
mock_adax_cloud.fetch_rooms_info.assert_called_once()
mock_adax_cloud.get_rooms.assert_called_once()
await snapshot_platform(
hass, entity_registry, snapshot, mock_cloud_config_entry.entry_id
)

View File

@ -54,7 +54,7 @@ from homeassistant.setup import async_setup_component
from .test_init import MOCK_ENVIRON from .test_init import MOCK_ENVIRON
from tests.common import load_json_object_fixture, mock_platform from tests.common import async_load_json_object_fixture, mock_platform
from tests.typing import ClientSessionGenerator, WebSocketGenerator from tests.typing import ClientSessionGenerator, WebSocketGenerator
TEST_BACKUP = supervisor_backups.Backup( TEST_BACKUP = supervisor_backups.Backup(
@ -1018,8 +1018,10 @@ async def test_reader_writer_create_addon_folder_error(
supervisor_client.jobs.get_job.side_effect = [ supervisor_client.jobs.get_job.side_effect = [
TEST_JOB_NOT_DONE, TEST_JOB_NOT_DONE,
supervisor_jobs.Job.from_dict( supervisor_jobs.Job.from_dict(
load_json_object_fixture( (
"backup_done_with_addon_folder_errors.json", DOMAIN await async_load_json_object_fixture(
hass, "backup_done_with_addon_folder_errors.json", DOMAIN
)
)["data"] )["data"]
), ),
] ]

View File

@ -176,8 +176,8 @@ async def test_hmip_dump_hap_config_services(
assert write_mock.mock_calls assert write_mock.mock_calls
async def test_setup_services_and_unload_services(hass: HomeAssistant) -> None: async def test_setup_services(hass: HomeAssistant) -> None:
"""Test setup services and unload services.""" """Test setup services."""
mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"} mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"}
MockConfigEntry(domain=DOMAIN, data=mock_config).add_to_hass(hass) MockConfigEntry(domain=DOMAIN, data=mock_config).add_to_hass(hass)
@ -201,46 +201,3 @@ async def test_setup_services_and_unload_services(hass: HomeAssistant) -> None:
assert len(config_entries) == 1 assert len(config_entries) == 1
await hass.config_entries.async_unload(config_entries[0].entry_id) await hass.config_entries.async_unload(config_entries[0].entry_id)
# Check services are removed
assert not hass.services.async_services().get(DOMAIN)
async def test_setup_two_haps_unload_one_by_one(hass: HomeAssistant) -> None:
"""Test setup two access points and unload one by one and check services."""
# Setup AP1
mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"}
MockConfigEntry(domain=DOMAIN, data=mock_config).add_to_hass(hass)
# Setup AP2
mock_config2 = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC1234", HMIPC_NAME: "name2"}
MockConfigEntry(domain=DOMAIN, data=mock_config2).add_to_hass(hass)
with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap:
instance = mock_hap.return_value
instance.async_setup = AsyncMock(return_value=True)
instance.home.id = "1"
instance.home.modelType = "mock-type"
instance.home.name = "mock-name"
instance.home.label = "mock-label"
instance.home.currentAPVersion = "mock-ap-version"
instance.async_reset = AsyncMock(return_value=True)
assert await async_setup_component(hass, DOMAIN, {})
hmipc_services = hass.services.async_services()[DOMAIN]
assert len(hmipc_services) == 9
config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(config_entries) == 2
# unload the first AP
await hass.config_entries.async_unload(config_entries[0].entry_id)
# services still exists
hmipc_services = hass.services.async_services()[DOMAIN]
assert len(hmipc_services) == 9
# unload the second AP
await hass.config_entries.async_unload(config_entries[1].entry_id)
# Check services are removed
assert not hass.services.async_services().get(DOMAIN)

View File

@ -15,7 +15,7 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DHCP_DATA, DISCOVERY_DATA, HOMEKIT_DATA, MOCK_SERIAL from .const import DHCP_DATA, DISCOVERY_DATA, HOMEKIT_DATA, MOCK_SERIAL
from tests.common import MockConfigEntry, load_json_object_fixture from tests.common import MockConfigEntry, async_load_json_object_fixture
@pytest.mark.usefixtures("mock_hunterdouglas_hub") @pytest.mark.usefixtures("mock_hunterdouglas_hub")
@ -330,7 +330,9 @@ async def test_form_unsupported_device(
# Simulate a gen 3 secondary hub # Simulate a gen 3 secondary hub
with patch( with patch(
"homeassistant.components.hunterdouglas_powerview.util.Hub.request_raw_data", "homeassistant.components.hunterdouglas_powerview.util.Hub.request_raw_data",
return_value=load_json_object_fixture("gen3/gateway/secondary.json", DOMAIN), return_value=await async_load_json_object_fixture(
hass, "gen3/gateway/secondary.json", DOMAIN
),
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],

View File

@ -29,7 +29,7 @@ async def test_nexia_sensor_switch(
hass: HomeAssistant, freezer: FrozenDateTimeFactory hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None: ) -> None:
"""Test NexiaRoomIQSensorSwitch.""" """Test NexiaRoomIQSensorSwitch."""
await async_init_integration(hass, house_fixture="nexia/sensors_xl1050_house.json") await async_init_integration(hass, house_fixture="sensors_xl1050_house.json")
sw1_id = f"{Platform.SWITCH}.center_nativezone_include_center" sw1_id = f"{Platform.SWITCH}.center_nativezone_include_center"
sw1 = {ATTR_ENTITY_ID: sw1_id} sw1 = {ATTR_ENTITY_ID: sw1_id}
sw2_id = f"{Platform.SWITCH}.center_nativezone_include_upstairs" sw2_id = f"{Platform.SWITCH}.center_nativezone_include_upstairs"

View File

@ -9,7 +9,7 @@ from homeassistant.components.nexia.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture from tests.common import MockConfigEntry, async_load_fixture
from tests.test_util.aiohttp import mock_aiohttp_client from tests.test_util.aiohttp import mock_aiohttp_client
@ -18,13 +18,13 @@ async def async_init_integration(
skip_setup: bool = False, skip_setup: bool = False,
exception: Exception | None = None, exception: Exception | None = None,
*, *,
house_fixture="nexia/mobile_houses_123456.json", house_fixture="mobile_houses_123456.json",
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Set up the nexia integration in Home Assistant.""" """Set up the nexia integration in Home Assistant."""
session_fixture = "nexia/session_123456.json" session_fixture = "session_123456.json"
sign_in_fixture = "nexia/sign_in.json" sign_in_fixture = "sign_in.json"
set_fan_speed_fixture = "nexia/set_fan_speed_2293892.json" set_fan_speed_fixture = "set_fan_speed_2293892.json"
with ( with (
mock_aiohttp_client() as mock_session, mock_aiohttp_client() as mock_session,
patch("nexia.home.load_or_create_uuid", return_value=uuid.uuid4()), patch("nexia.home.load_or_create_uuid", return_value=uuid.uuid4()),
@ -40,19 +40,20 @@ async def async_init_integration(
) )
else: else:
mock_session.post( mock_session.post(
nexia.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture) nexia.API_MOBILE_SESSION_URL,
text=await async_load_fixture(hass, session_fixture, DOMAIN),
) )
mock_session.get( mock_session.get(
nexia.API_MOBILE_HOUSES_URL.format(house_id=123456), nexia.API_MOBILE_HOUSES_URL.format(house_id=123456),
text=load_fixture(house_fixture), text=await async_load_fixture(hass, house_fixture, DOMAIN),
) )
mock_session.post( mock_session.post(
nexia.API_MOBILE_ACCOUNTS_SIGN_IN_URL, nexia.API_MOBILE_ACCOUNTS_SIGN_IN_URL,
text=load_fixture(sign_in_fixture), text=await async_load_fixture(hass, sign_in_fixture, DOMAIN),
) )
mock_session.post( mock_session.post(
"https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed", "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed",
text=load_fixture(set_fan_speed_fixture), text=await async_load_fixture(hass, set_fan_speed_fixture, DOMAIN),
) )
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,

View File

@ -9,8 +9,8 @@ from .mock import MOCK_INFO, setup_nuki_integration
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
load_json_array_fixture, async_load_json_array_fixture,
load_json_object_fixture, async_load_json_object_fixture,
) )
@ -21,15 +21,19 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry:
mock.get("http://1.1.1.1:8080/info", json=MOCK_INFO) mock.get("http://1.1.1.1:8080/info", json=MOCK_INFO)
mock.get( mock.get(
"http://1.1.1.1:8080/list", "http://1.1.1.1:8080/list",
json=load_json_array_fixture("list.json", DOMAIN), json=await async_load_json_array_fixture(hass, "list.json", DOMAIN),
) )
mock.get( mock.get(
"http://1.1.1.1:8080/callback/list", "http://1.1.1.1:8080/callback/list",
json=load_json_object_fixture("callback_list.json", DOMAIN), json=await async_load_json_object_fixture(
hass, "callback_list.json", DOMAIN
),
) )
mock.get( mock.get(
"http://1.1.1.1:8080/callback/add", "http://1.1.1.1:8080/callback/add",
json=load_json_object_fixture("callback_add.json", DOMAIN), json=await async_load_json_object_fixture(
hass, "callback_add.json", DOMAIN
),
) )
entry = await setup_nuki_integration(hass) entry = await setup_nuki_integration(hass)
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)

View File

@ -8,7 +8,7 @@ from homeassistant.components.overkiz.const import DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry, load_json_object_fixture from tests.common import MockConfigEntry, async_load_json_object_fixture
from tests.components.diagnostics import ( from tests.components.diagnostics import (
get_diagnostics_for_config_entry, get_diagnostics_for_config_entry,
get_diagnostics_for_device, get_diagnostics_for_device,
@ -23,7 +23,9 @@ async def test_diagnostics(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test diagnostics.""" """Test diagnostics."""
diagnostic_data = load_json_object_fixture("overkiz/setup_tahoma_switch.json") diagnostic_data = await async_load_json_object_fixture(
hass, "setup_tahoma_switch.json", DOMAIN
)
with patch.multiple( with patch.multiple(
"pyoverkiz.client.OverkizClient", "pyoverkiz.client.OverkizClient",
@ -44,7 +46,9 @@ async def test_device_diagnostics(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test device diagnostics.""" """Test device diagnostics."""
diagnostic_data = load_json_object_fixture("overkiz/setup_tahoma_switch.json") diagnostic_data = await async_load_json_object_fixture(
hass, "setup_tahoma_switch.json", DOMAIN
)
device = device_registry.async_get_device( device = device_registry.async_get_device(
identifiers={(DOMAIN, "rts://****-****-6867/16756006")} identifiers={(DOMAIN, "rts://****-****-6867/16756006")}

View File

@ -12,7 +12,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from tests.common import MockConfigEntry, load_fixture from tests.common import MockConfigEntry, async_load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
USERNAME = "user" USERNAME = "user"
@ -53,39 +53,41 @@ def create_entry(hass: HomeAssistant) -> MockConfigEntry:
return entry return entry
async def set_aioclient_responses(aioclient_mock: AiohttpClientMocker) -> None: async def set_aioclient_responses(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Set AioClient responses.""" """Set AioClient responses."""
aioclient_mock.get( aioclient_mock.get(
f"{BASE_URL}devices/{DEVICE_ID}/info/", f"{BASE_URL}devices/{DEVICE_ID}/info/",
text=load_fixture("skybell/device_info.json"), text=await async_load_fixture(hass, "device_info.json", DOMAIN),
) )
aioclient_mock.get( aioclient_mock.get(
f"{BASE_URL}devices/{DEVICE_ID}/settings/", f"{BASE_URL}devices/{DEVICE_ID}/settings/",
text=load_fixture("skybell/device_settings.json"), text=await async_load_fixture(hass, "device_settings.json", DOMAIN),
) )
aioclient_mock.get( aioclient_mock.get(
f"{BASE_URL}devices/{DEVICE_ID}/activities/", f"{BASE_URL}devices/{DEVICE_ID}/activities/",
text=load_fixture("skybell/activities.json"), text=await async_load_fixture(hass, "activities.json", DOMAIN),
) )
aioclient_mock.get( aioclient_mock.get(
f"{BASE_URL}devices/", f"{BASE_URL}devices/",
text=load_fixture("skybell/device.json"), text=await async_load_fixture(hass, "device.json", DOMAIN),
) )
aioclient_mock.get( aioclient_mock.get(
USERS_ME_URL, USERS_ME_URL,
text=load_fixture("skybell/me.json"), text=await async_load_fixture(hass, "me.json", DOMAIN),
) )
aioclient_mock.post( aioclient_mock.post(
f"{BASE_URL}login/", f"{BASE_URL}login/",
text=load_fixture("skybell/login.json"), text=await async_load_fixture(hass, "login.json", DOMAIN),
) )
aioclient_mock.get( aioclient_mock.get(
f"{BASE_URL}devices/{DEVICE_ID}/activities/1234567890ab1234567890ac/video/", f"{BASE_URL}devices/{DEVICE_ID}/activities/1234567890ab1234567890ac/video/",
text=load_fixture("skybell/video.json"), text=await async_load_fixture(hass, "video.json", DOMAIN),
) )
aioclient_mock.get( aioclient_mock.get(
f"{BASE_URL}devices/{DEVICE_ID}/avatar/", f"{BASE_URL}devices/{DEVICE_ID}/avatar/",
text=load_fixture("skybell/avatar.json"), text=await async_load_fixture(hass, "avatar.json", DOMAIN),
) )
aioclient_mock.get( aioclient_mock.get(
f"https://v3-production-devices-avatar.s3.us-west-2.amazonaws.com/{DEVICE_ID}.jpg", f"https://v3-production-devices-avatar.s3.us-west-2.amazonaws.com/{DEVICE_ID}.jpg",
@ -96,12 +98,12 @@ async def set_aioclient_responses(aioclient_mock: AiohttpClientMocker) -> None:
@pytest.fixture @pytest.fixture
async def connection(aioclient_mock: AiohttpClientMocker) -> None: async def connection(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None:
"""Fixture for good connection responses.""" """Fixture for good connection responses."""
await set_aioclient_responses(aioclient_mock) await set_aioclient_responses(hass, aioclient_mock)
def create_skybell(hass: HomeAssistant) -> Skybell: async def create_skybell(hass: HomeAssistant) -> Skybell:
"""Create Skybell object.""" """Create Skybell object."""
skybell = Skybell( skybell = Skybell(
username=USERNAME, username=USERNAME,
@ -109,14 +111,15 @@ def create_skybell(hass: HomeAssistant) -> Skybell:
get_devices=True, get_devices=True,
session=async_get_clientsession(hass), session=async_get_clientsession(hass),
) )
skybell._cache = orjson.loads(load_fixture("skybell/cache.json")) skybell._cache = orjson.loads(await async_load_fixture(hass, "cache.json", DOMAIN))
return skybell return skybell
def mock_skybell(hass: HomeAssistant): async def mock_skybell(hass: HomeAssistant):
"""Mock Skybell object.""" """Mock Skybell object."""
return patch( return patch(
"homeassistant.components.skybell.Skybell", return_value=create_skybell(hass) "homeassistant.components.skybell.Skybell",
return_value=await create_skybell(hass),
) )
@ -124,7 +127,7 @@ async def async_init_integration(hass: HomeAssistant) -> MockConfigEntry:
"""Set up the Skybell integration in Home Assistant.""" """Set up the Skybell integration in Home Assistant."""
config_entry = create_entry(hass) config_entry = create_entry(hass)
with mock_skybell(hass), patch("aioskybell.utils.async_save_cache"): with await mock_skybell(hass), patch("aioskybell.utils.async_save_cache"):
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -12,7 +12,7 @@ from homeassistant.helpers import entity_registry as er
from . import configure_integration from . import configure_integration
from tests.common import load_json_object_fixture, snapshot_platform from tests.common import async_load_json_object_fixture, snapshot_platform
async def test_meter( async def test_meter(
@ -33,7 +33,9 @@ async def test_meter(
hubDeviceId="test-hub-id", hubDeviceId="test-hub-id",
), ),
] ]
mock_get_status.return_value = load_json_object_fixture("meter_status.json", DOMAIN) mock_get_status.return_value = await async_load_json_object_fixture(
hass, "meter_status.json", DOMAIN
)
with patch("homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.SENSOR]): with patch("homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.SENSOR]):
entry = await configure_integration(hass) entry = await configure_integration(hass)

View File

@ -1,6 +1,7 @@
"""Test fixtures for TP-Link Omada integration.""" """Test fixtures for TP-Link Omada integration."""
from collections.abc import AsyncIterable, Generator from collections.abc import AsyncGenerator, Generator
from functools import partial
import json import json
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
@ -23,7 +24,7 @@ from homeassistant.components.tplink_omada.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture from tests.common import MockConfigEntry, async_load_fixture
@pytest.fixture @pytest.fixture
@ -53,29 +54,33 @@ def mock_setup_entry() -> Generator[AsyncMock]:
@pytest.fixture @pytest.fixture
def mock_omada_site_client() -> Generator[AsyncMock]: async def mock_omada_site_client(hass: HomeAssistant) -> AsyncGenerator[AsyncMock]:
"""Mock Omada site client.""" """Mock Omada site client."""
site_client = MagicMock() site_client = MagicMock()
gateway_data = json.loads(load_fixture("gateway-TL-ER7212PC.json", DOMAIN)) gateway_data = json.loads(
await async_load_fixture(hass, "gateway-TL-ER7212PC.json", DOMAIN)
)
gateway = OmadaGateway(gateway_data) gateway = OmadaGateway(gateway_data)
site_client.get_gateway = AsyncMock(return_value=gateway) site_client.get_gateway = AsyncMock(return_value=gateway)
switch1_data = json.loads(load_fixture("switch-TL-SG3210XHP-M2.json", DOMAIN)) switch1_data = json.loads(
await async_load_fixture(hass, "switch-TL-SG3210XHP-M2.json", DOMAIN)
)
switch1 = OmadaSwitch(switch1_data) switch1 = OmadaSwitch(switch1_data)
site_client.get_switches = AsyncMock(return_value=[switch1]) site_client.get_switches = AsyncMock(return_value=[switch1])
devices_data = json.loads(load_fixture("devices.json", DOMAIN)) devices_data = json.loads(await async_load_fixture(hass, "devices.json", DOMAIN))
devices = [OmadaListDevice(d) for d in devices_data] devices = [OmadaListDevice(d) for d in devices_data]
site_client.get_devices = AsyncMock(return_value=devices) site_client.get_devices = AsyncMock(return_value=devices)
switch1_ports_data = json.loads( switch1_ports_data = json.loads(
load_fixture("switch-ports-TL-SG3210XHP-M2.json", DOMAIN) await async_load_fixture(hass, "switch-ports-TL-SG3210XHP-M2.json", DOMAIN)
) )
switch1_ports = [OmadaSwitchPortDetails(p) for p in switch1_ports_data] switch1_ports = [OmadaSwitchPortDetails(p) for p in switch1_ports_data]
site_client.get_switch_ports = AsyncMock(return_value=switch1_ports) site_client.get_switch_ports = AsyncMock(return_value=switch1_ports)
async def async_empty() -> AsyncIterable: async def async_empty() -> AsyncGenerator:
for c in (): for c in ():
yield c yield c
@ -85,24 +90,30 @@ def mock_omada_site_client() -> Generator[AsyncMock]:
@pytest.fixture @pytest.fixture
def mock_omada_clients_only_site_client() -> Generator[AsyncMock]: def mock_omada_clients_only_site_client(hass: HomeAssistant) -> Generator[AsyncMock]:
"""Mock Omada site client containing only client connection data.""" """Mock Omada site client containing only client connection data."""
site_client = MagicMock() site_client = MagicMock()
site_client.get_switches = AsyncMock(return_value=[]) site_client.get_switches = AsyncMock(return_value=[])
site_client.get_devices = AsyncMock(return_value=[]) site_client.get_devices = AsyncMock(return_value=[])
site_client.get_switch_ports = AsyncMock(return_value=[]) site_client.get_switch_ports = AsyncMock(return_value=[])
site_client.get_client = AsyncMock(side_effect=_get_mock_client) site_client.get_client = AsyncMock(side_effect=partial(_get_mock_client, hass))
site_client.get_known_clients.side_effect = _get_mock_known_clients site_client.get_known_clients.side_effect = partial(_get_mock_known_clients, hass)
site_client.get_connected_clients.side_effect = _get_mock_connected_clients site_client.get_connected_clients.side_effect = partial(
_get_mock_connected_clients, hass
)
return site_client return site_client
async def _get_mock_known_clients() -> AsyncIterable[OmadaNetworkClient]: async def _get_mock_known_clients(
hass: HomeAssistant,
) -> AsyncGenerator[OmadaNetworkClient]:
"""Mock known clients of the Omada network.""" """Mock known clients of the Omada network."""
known_clients_data = json.loads(load_fixture("known-clients.json", DOMAIN)) known_clients_data = json.loads(
await async_load_fixture(hass, "known-clients.json", DOMAIN)
)
for c in known_clients_data: for c in known_clients_data:
if c["wireless"]: if c["wireless"]:
yield OmadaWirelessClient(c) yield OmadaWirelessClient(c)
@ -110,9 +121,13 @@ async def _get_mock_known_clients() -> AsyncIterable[OmadaNetworkClient]:
yield OmadaWiredClient(c) yield OmadaWiredClient(c)
async def _get_mock_connected_clients() -> AsyncIterable[OmadaConnectedClient]: async def _get_mock_connected_clients(
hass: HomeAssistant,
) -> AsyncGenerator[OmadaConnectedClient]:
"""Mock connected clients of the Omada network.""" """Mock connected clients of the Omada network."""
connected_clients_data = json.loads(load_fixture("connected-clients.json", DOMAIN)) connected_clients_data = json.loads(
await async_load_fixture(hass, "connected-clients.json", DOMAIN)
)
for c in connected_clients_data: for c in connected_clients_data:
if c["wireless"]: if c["wireless"]:
yield OmadaWirelessClient(c) yield OmadaWirelessClient(c)
@ -120,9 +135,11 @@ async def _get_mock_connected_clients() -> AsyncIterable[OmadaConnectedClient]:
yield OmadaWiredClient(c) yield OmadaWiredClient(c)
def _get_mock_client(mac: str) -> OmadaNetworkClient: async def _get_mock_client(hass: HomeAssistant, mac: str) -> OmadaNetworkClient:
"""Mock an Omada client.""" """Mock an Omada client."""
connected_clients_data = json.loads(load_fixture("connected-clients.json", DOMAIN)) connected_clients_data = json.loads(
await async_load_fixture(hass, "connected-clients.json", DOMAIN)
)
for c in connected_clients_data: for c in connected_clients_data:
if c["mac"] == mac: if c["mac"] == mac:

View File

@ -8,8 +8,8 @@ from homeassistant.core import HomeAssistant
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
load_json_array_fixture, async_load_json_array_fixture,
load_json_object_fixture, async_load_json_object_fixture,
) )
@ -18,16 +18,22 @@ async def init_component(hass: HomeAssistant) -> MockConfigEntry:
with requests_mock.Mocker() as mock: with requests_mock.Mocker() as mock:
mock.get( mock.get(
"http://1.1.1.1/d", "http://1.1.1.1/d",
json=load_json_object_fixture("device.json", youless.DOMAIN), json=await async_load_json_object_fixture(
hass, "device.json", youless.DOMAIN
),
) )
mock.get( mock.get(
"http://1.1.1.1/e", "http://1.1.1.1/e",
json=load_json_array_fixture("enologic.json", youless.DOMAIN), json=await async_load_json_array_fixture(
hass, "enologic.json", youless.DOMAIN
),
headers={"Content-Type": "application/json"}, headers={"Content-Type": "application/json"},
) )
mock.get( mock.get(
"http://1.1.1.1/f", "http://1.1.1.1/f",
json=load_json_object_fixture("phase.json", youless.DOMAIN), json=await async_load_json_object_fixture(
hass, "phase.json", youless.DOMAIN
),
headers={"Content-Type": "application/json"}, headers={"Content-Type": "application/json"},
) )