mirror of
https://github.com/home-assistant/core.git
synced 2025-12-19 18:38:58 +00:00
Add Lunatone gateway integration (#149182)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -326,6 +326,7 @@ homeassistant.components.london_underground.*
|
||||
homeassistant.components.lookin.*
|
||||
homeassistant.components.lovelace.*
|
||||
homeassistant.components.luftdaten.*
|
||||
homeassistant.components.lunatone.*
|
||||
homeassistant.components.madvr.*
|
||||
homeassistant.components.manual.*
|
||||
homeassistant.components.mastodon.*
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -910,6 +910,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/luci/ @mzdrale
|
||||
/homeassistant/components/luftdaten/ @fabaff @frenck
|
||||
/tests/components/luftdaten/ @fabaff @frenck
|
||||
/homeassistant/components/lunatone/ @MoonDevLT
|
||||
/tests/components/lunatone/ @MoonDevLT
|
||||
/homeassistant/components/lupusec/ @majuss @suaveolent
|
||||
/tests/components/lupusec/ @majuss @suaveolent
|
||||
/homeassistant/components/lutron/ @cdheiser @wilburCForce
|
||||
|
||||
64
homeassistant/components/lunatone/__init__.py
Normal file
64
homeassistant/components/lunatone/__init__.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""The Lunatone integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
from lunatone_rest_api_client import Auth, Devices, Info
|
||||
|
||||
from homeassistant.const import CONF_URL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
LunatoneConfigEntry,
|
||||
LunatoneData,
|
||||
LunatoneDevicesDataUpdateCoordinator,
|
||||
LunatoneInfoDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS: Final[list[Platform]] = [Platform.LIGHT]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LunatoneConfigEntry) -> bool:
|
||||
"""Set up Lunatone from a config entry."""
|
||||
auth_api = Auth(async_get_clientsession(hass), entry.data[CONF_URL])
|
||||
info_api = Info(auth_api)
|
||||
devices_api = Devices(auth_api)
|
||||
|
||||
coordinator_info = LunatoneInfoDataUpdateCoordinator(hass, entry, info_api)
|
||||
await coordinator_info.async_config_entry_first_refresh()
|
||||
|
||||
if info_api.serial_number is None:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="missing_device_info"
|
||||
)
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, str(info_api.serial_number))},
|
||||
name=info_api.name,
|
||||
manufacturer="Lunatone",
|
||||
sw_version=info_api.version,
|
||||
hw_version=info_api.data.device.pcb,
|
||||
configuration_url=entry.data[CONF_URL],
|
||||
serial_number=str(info_api.serial_number),
|
||||
model_id=(
|
||||
f"{info_api.data.device.article_number}{info_api.data.device.article_info}"
|
||||
),
|
||||
)
|
||||
|
||||
coordinator_devices = LunatoneDevicesDataUpdateCoordinator(hass, entry, devices_api)
|
||||
await coordinator_devices.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = LunatoneData(coordinator_info, coordinator_devices)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: LunatoneConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
83
homeassistant/components/lunatone/config_flow.py
Normal file
83
homeassistant/components/lunatone/config_flow.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Config flow for Lunatone."""
|
||||
|
||||
from typing import Any, Final
|
||||
|
||||
import aiohttp
|
||||
from lunatone_rest_api_client import Auth, Info
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
DATA_SCHEMA: Final[vol.Schema] = vol.Schema(
|
||||
{vol.Required(CONF_URL, default="http://"): cv.string},
|
||||
)
|
||||
|
||||
|
||||
def compose_title(name: str | None, serial_number: int) -> str:
|
||||
"""Compose a title string from a given name and serial number."""
|
||||
return f"{name or 'DALI Gateway'} {serial_number}"
|
||||
|
||||
|
||||
class LunatoneConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Lunatone config flow."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
url = user_input[CONF_URL]
|
||||
data = {CONF_URL: url}
|
||||
self._async_abort_entries_match(data)
|
||||
auth_api = Auth(
|
||||
session=async_get_clientsession(self.hass),
|
||||
base_url=url,
|
||||
)
|
||||
info_api = Info(auth_api)
|
||||
try:
|
||||
await info_api.async_update()
|
||||
except aiohttp.InvalidUrlClientError:
|
||||
errors["base"] = "invalid_url"
|
||||
except aiohttp.ClientConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
if info_api.data is None or info_api.serial_number is None:
|
||||
errors["base"] = "missing_device_info"
|
||||
else:
|
||||
await self.async_set_unique_id(str(info_api.serial_number))
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates=data,
|
||||
title=compose_title(info_api.name, info_api.serial_number),
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=compose_title(info_api.name, info_api.serial_number),
|
||||
data={CONF_URL: url},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a reconfiguration flow initialized by the user."""
|
||||
return await self.async_step_user(user_input)
|
||||
5
homeassistant/components/lunatone/const.py
Normal file
5
homeassistant/components/lunatone/const.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Constants for the Lunatone integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "lunatone"
|
||||
101
homeassistant/components/lunatone/coordinator.py
Normal file
101
homeassistant/components/lunatone/coordinator.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Coordinator for handling data fetching and updates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from lunatone_rest_api_client import Device, Devices, Info
|
||||
from lunatone_rest_api_client.models import InfoData
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DEVICES_SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LunatoneData:
|
||||
"""Data for Lunatone integration."""
|
||||
|
||||
coordinator_info: LunatoneInfoDataUpdateCoordinator
|
||||
coordinator_devices: LunatoneDevicesDataUpdateCoordinator
|
||||
|
||||
|
||||
type LunatoneConfigEntry = ConfigEntry[LunatoneData]
|
||||
|
||||
|
||||
class LunatoneInfoDataUpdateCoordinator(DataUpdateCoordinator[InfoData]):
|
||||
"""Data update coordinator for Lunatone info."""
|
||||
|
||||
config_entry: LunatoneConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: LunatoneConfigEntry, info_api: Info
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"{DOMAIN}-info",
|
||||
always_update=False,
|
||||
)
|
||||
self.info_api = info_api
|
||||
|
||||
async def _async_update_data(self) -> InfoData:
|
||||
"""Update info data."""
|
||||
try:
|
||||
await self.info_api.async_update()
|
||||
except aiohttp.ClientConnectionError as ex:
|
||||
raise UpdateFailed(
|
||||
"Unable to retrieve info data from Lunatone REST API"
|
||||
) from ex
|
||||
|
||||
if self.info_api.data is None:
|
||||
raise UpdateFailed("Did not receive info data from Lunatone REST API")
|
||||
return self.info_api.data
|
||||
|
||||
|
||||
class LunatoneDevicesDataUpdateCoordinator(DataUpdateCoordinator[dict[int, Device]]):
|
||||
"""Data update coordinator for Lunatone devices."""
|
||||
|
||||
config_entry: LunatoneConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: LunatoneConfigEntry,
|
||||
devices_api: Devices,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"{DOMAIN}-devices",
|
||||
always_update=False,
|
||||
update_interval=DEFAULT_DEVICES_SCAN_INTERVAL,
|
||||
)
|
||||
self.devices_api = devices_api
|
||||
|
||||
async def _async_update_data(self) -> dict[int, Device]:
|
||||
"""Update devices data."""
|
||||
try:
|
||||
await self.devices_api.async_update()
|
||||
except aiohttp.ClientConnectionError as ex:
|
||||
raise UpdateFailed(
|
||||
"Unable to retrieve devices data from Lunatone REST API"
|
||||
) from ex
|
||||
|
||||
if self.devices_api.data is None:
|
||||
raise UpdateFailed("Did not receive devices data from Lunatone REST API")
|
||||
|
||||
return {device.id: device for device in self.devices_api.devices}
|
||||
103
homeassistant/components/lunatone/light.py
Normal file
103
homeassistant/components/lunatone/light.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Platform for Lunatone light integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LunatoneConfigEntry, LunatoneDevicesDataUpdateCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
STATUS_UPDATE_DELAY = 0.04
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: LunatoneConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Lunatone Light platform."""
|
||||
coordinator_info = config_entry.runtime_data.coordinator_info
|
||||
coordinator_devices = config_entry.runtime_data.coordinator_devices
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
LunatoneLight(
|
||||
coordinator_devices, device_id, coordinator_info.data.device.serial
|
||||
)
|
||||
for device_id in coordinator_devices.data
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class LunatoneLight(
|
||||
CoordinatorEntity[LunatoneDevicesDataUpdateCoordinator], LightEntity
|
||||
):
|
||||
"""Representation of a Lunatone light."""
|
||||
|
||||
_attr_color_mode = ColorMode.ONOFF
|
||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LunatoneDevicesDataUpdateCoordinator,
|
||||
device_id: int,
|
||||
interface_serial_number: int,
|
||||
) -> None:
|
||||
"""Initialize a LunatoneLight."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
self._device_id = device_id
|
||||
self._interface_serial_number = interface_serial_number
|
||||
self._device = self.coordinator.data.get(self._device_id)
|
||||
self._attr_unique_id = f"{interface_serial_number}-device{device_id}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
assert self.unique_id
|
||||
name = self._device.name if self._device is not None else None
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
name=name,
|
||||
via_device=(DOMAIN, str(self._interface_serial_number)),
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self._device is not None
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if light is on."""
|
||||
return self._device is not None and self._device.is_on
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._device = self.coordinator.data.get(self._device_id)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on."""
|
||||
assert self._device
|
||||
await self._device.switch_on()
|
||||
await asyncio.sleep(STATUS_UPDATE_DELAY)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn off."""
|
||||
assert self._device
|
||||
await self._device.switch_off()
|
||||
await asyncio.sleep(STATUS_UPDATE_DELAY)
|
||||
await self.coordinator.async_refresh()
|
||||
11
homeassistant/components/lunatone/manifest.json
Normal file
11
homeassistant/components/lunatone/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "lunatone",
|
||||
"name": "Lunatone",
|
||||
"codeowners": ["@MoonDevLT"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/lunatone",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["lunatone-rest-api-client==0.4.8"]
|
||||
}
|
||||
82
homeassistant/components/lunatone/quality_scale.yaml
Normal file
82
homeassistant/components/lunatone/quality_scale.yaml
Normal file
@@ -0,0 +1,82 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration has only one platform which uses a coordinator.
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities of this integration does not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: no actions
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: No options to configure
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not require authentication.
|
||||
test-coverage: done
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: todo
|
||||
comment: Discovery not yet supported
|
||||
discovery:
|
||||
status: todo
|
||||
comment: Discovery not yet supported
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
||||
36
homeassistant/components/lunatone/strings.json
Normal file
36
homeassistant/components/lunatone/strings.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
},
|
||||
"user": {
|
||||
"description": "Connect to the API of your Lunatone DALI IoT Gateway.",
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "The URL of the Lunatone gateway device."
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Update the URL.",
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "[%key:component::lunatone::config::step::user::data_description::url%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_url": "Failed to connect. Check the URL and if the device is connected to power",
|
||||
"missing_device_info": "Failed to read device information. Check the network connection of the device"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -370,6 +370,7 @@ FLOWS = {
|
||||
"lookin",
|
||||
"loqed",
|
||||
"luftdaten",
|
||||
"lunatone",
|
||||
"lupusec",
|
||||
"lutron",
|
||||
"lutron_caseta",
|
||||
|
||||
@@ -3727,6 +3727,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"lunatone": {
|
||||
"name": "Lunatone",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"lupusec": {
|
||||
"name": "Lupus Electronics LUPUSEC",
|
||||
"integration_type": "hub",
|
||||
|
||||
10
mypy.ini
generated
10
mypy.ini
generated
@@ -3016,6 +3016,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.lunatone.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.madvr.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@@ -1399,6 +1399,9 @@ loqedAPI==2.1.10
|
||||
# homeassistant.components.luftdaten
|
||||
luftdaten==0.7.4
|
||||
|
||||
# homeassistant.components.lunatone
|
||||
lunatone-rest-api-client==0.4.8
|
||||
|
||||
# homeassistant.components.lupusec
|
||||
lupupy==0.3.2
|
||||
|
||||
|
||||
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@@ -1200,6 +1200,9 @@ loqedAPI==2.1.10
|
||||
# homeassistant.components.luftdaten
|
||||
luftdaten==0.7.4
|
||||
|
||||
# homeassistant.components.lunatone
|
||||
lunatone-rest-api-client==0.4.8
|
||||
|
||||
# homeassistant.components.lupusec
|
||||
lupupy==0.3.2
|
||||
|
||||
|
||||
76
tests/components/lunatone/__init__.py
Normal file
76
tests/components/lunatone/__init__.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Tests for the Lunatone integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
from lunatone_rest_api_client.models import (
|
||||
DeviceData,
|
||||
DeviceInfoData,
|
||||
DevicesData,
|
||||
FeaturesStatus,
|
||||
InfoData,
|
||||
)
|
||||
from lunatone_rest_api_client.models.common import ColorRGBData, ColorWAFData, Status
|
||||
from lunatone_rest_api_client.models.devices import DeviceStatus
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
BASE_URL: Final = "http://10.0.0.131"
|
||||
SERIAL_NUMBER: Final = 12345
|
||||
VERSION: Final = "v1.14.1/1.4.3"
|
||||
|
||||
DEVICE_DATA_LIST: Final[list[DeviceData]] = [
|
||||
DeviceData(
|
||||
id=1,
|
||||
name="Device 1",
|
||||
available=True,
|
||||
status=DeviceStatus(),
|
||||
features=FeaturesStatus(
|
||||
switchable=Status[bool](status=False),
|
||||
dimmable=Status[float](status=0.0),
|
||||
colorKelvin=Status[int](status=1000),
|
||||
colorRGB=Status[ColorRGBData](status=ColorRGBData(r=0, g=0, b=0)),
|
||||
colorWAF=Status[ColorWAFData](status=ColorWAFData(w=0, a=0, f=0)),
|
||||
),
|
||||
address=0,
|
||||
line=0,
|
||||
),
|
||||
DeviceData(
|
||||
id=2,
|
||||
name="Device 2",
|
||||
available=True,
|
||||
status=DeviceStatus(),
|
||||
features=FeaturesStatus(
|
||||
switchable=Status[bool](status=False),
|
||||
dimmable=Status[float](status=0.0),
|
||||
colorKelvin=Status[int](status=1000),
|
||||
colorRGB=Status[ColorRGBData](status=ColorRGBData(r=0, g=0, b=0)),
|
||||
colorWAF=Status[ColorWAFData](status=ColorWAFData(w=0, a=0, f=0)),
|
||||
),
|
||||
address=1,
|
||||
line=0,
|
||||
),
|
||||
]
|
||||
DEVICES_DATA: Final[DevicesData] = DevicesData(devices=DEVICE_DATA_LIST)
|
||||
INFO_DATA: Final[InfoData] = InfoData(
|
||||
name="Test",
|
||||
version=VERSION,
|
||||
device=DeviceInfoData(
|
||||
serial=SERIAL_NUMBER,
|
||||
gtin=192837465,
|
||||
pcb="2a",
|
||||
articleNumber=87654321,
|
||||
productionYear=20,
|
||||
productionWeek=1,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Set up the Lunatone integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
82
tests/components/lunatone/conftest.py
Normal file
82
tests/components/lunatone/conftest.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Fixtures for Lunatone tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||
|
||||
from lunatone_rest_api_client import Device, Devices
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.lunatone.const import DOMAIN
|
||||
from homeassistant.const import CONF_URL
|
||||
|
||||
from . import BASE_URL, DEVICES_DATA, INFO_DATA, SERIAL_NUMBER
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.lunatone.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_lunatone_devices() -> Generator[AsyncMock]:
|
||||
"""Mock a Lunatone devices object."""
|
||||
|
||||
def build_devices_mock(devices: Devices):
|
||||
device_list = []
|
||||
for device_data in devices.data.devices:
|
||||
device = AsyncMock(spec=Device)
|
||||
device.data = device_data
|
||||
device.id = device.data.id
|
||||
device.name = device.data.name
|
||||
device.is_on = device.data.features.switchable.status
|
||||
device_list.append(device)
|
||||
return device_list
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.lunatone.Devices", autospec=True
|
||||
) as mock_devices:
|
||||
devices = mock_devices.return_value
|
||||
devices.data = DEVICES_DATA
|
||||
type(devices).devices = PropertyMock(
|
||||
side_effect=lambda d=devices: build_devices_mock(d)
|
||||
)
|
||||
yield devices
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_lunatone_info() -> Generator[AsyncMock]:
|
||||
"""Mock a Lunatone info object."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.lunatone.Info",
|
||||
autospec=True,
|
||||
) as mock_info,
|
||||
patch(
|
||||
"homeassistant.components.lunatone.config_flow.Info",
|
||||
new=mock_info,
|
||||
),
|
||||
):
|
||||
info = mock_info.return_value
|
||||
info.data = INFO_DATA
|
||||
info.name = info.data.name
|
||||
info.version = info.data.version
|
||||
info.serial_number = info.data.device.serial
|
||||
yield info
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title=f"Lunatone {SERIAL_NUMBER}",
|
||||
domain=DOMAIN,
|
||||
data={CONF_URL: BASE_URL},
|
||||
unique_id=str(SERIAL_NUMBER),
|
||||
)
|
||||
115
tests/components/lunatone/snapshots/test_light.ambr
Normal file
115
tests/components/lunatone/snapshots/test_light.ambr
Normal file
@@ -0,0 +1,115 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[light.device_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.device_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'lunatone',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '12345-device1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'color_mode': None,
|
||||
'friendly_name': 'Device 1',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.device_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_2-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.device_2',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'lunatone',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '12345-device2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'color_mode': None,
|
||||
'friendly_name': 'Device 2',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.device_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
184
tests/components/lunatone/test_config_flow.py
Normal file
184
tests/components/lunatone/test_config_flow.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""Define tests for the Lunatone config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.lunatone.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import BASE_URL, SERIAL_NUMBER
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_full_flow(
|
||||
hass: HomeAssistant, mock_lunatone_info: AsyncMock, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test full user flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_URL: BASE_URL},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"Test {SERIAL_NUMBER}"
|
||||
assert result["data"] == {CONF_URL: BASE_URL}
|
||||
|
||||
|
||||
async def test_full_flow_fail_because_of_missing_device_infos(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
) -> None:
|
||||
"""Test full flow."""
|
||||
mock_lunatone_info.data = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_URL: BASE_URL},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "missing_device_info"}
|
||||
|
||||
|
||||
async def test_device_already_configured(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test that the flow is aborted when the device is already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "user"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_URL: BASE_URL},
|
||||
)
|
||||
|
||||
assert result2.get("type") is FlowResultType.ABORT
|
||||
assert result2.get("reason") == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_error"),
|
||||
[
|
||||
(aiohttp.InvalidUrlClientError(BASE_URL), "invalid_url"),
|
||||
(aiohttp.ClientConnectionError(), "cannot_connect"),
|
||||
],
|
||||
)
|
||||
async def test_user_step_fail_with_error(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
exception: Exception,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""Test user step with an error."""
|
||||
mock_lunatone_info.async_update.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_URL: BASE_URL},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
|
||||
mock_lunatone_info.async_update.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_URL: BASE_URL},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"Test {SERIAL_NUMBER}"
|
||||
assert result["data"] == {CONF_URL: BASE_URL}
|
||||
|
||||
|
||||
async def test_reconfigure(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reconfigure flow."""
|
||||
url = "http://10.0.0.100"
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_URL: url}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data == {CONF_URL: url}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_error"),
|
||||
[
|
||||
(aiohttp.InvalidUrlClientError(BASE_URL), "invalid_url"),
|
||||
(aiohttp.ClientConnectionError(), "cannot_connect"),
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_fail_with_error(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""Test reconfigure flow with an error."""
|
||||
url = "http://10.0.0.100"
|
||||
|
||||
mock_lunatone_info.async_update.side_effect = exception
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_URL: url}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
|
||||
mock_lunatone_info.async_update.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_URL: url}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data == {CONF_URL: url}
|
||||
133
tests/components/lunatone/test_init.py
Normal file
133
tests/components/lunatone/test_init.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Tests for the Lunatone integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.components.lunatone.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import BASE_URL, VERSION, setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_load_unload_config_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test the Lunatone configuration entry loading/unloading."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, mock_config_entry.unique_id)}
|
||||
)
|
||||
assert device_entry is not None
|
||||
assert device_entry.manufacturer == "Lunatone"
|
||||
assert device_entry.sw_version == VERSION
|
||||
assert device_entry.configuration_url == BASE_URL
|
||||
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not hass.data.get(DOMAIN)
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_config_entry_not_ready_info_api_fail(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Lunatone configuration entry not ready due to a failure in the info API."""
|
||||
mock_lunatone_info.async_update.side_effect = aiohttp.ClientConnectionError()
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_lunatone_info.async_update.assert_called_once()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
mock_lunatone_info.async_update.side_effect = None
|
||||
|
||||
await hass.config_entries.async_reload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_lunatone_info.async_update.assert_called()
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_config_entry_not_ready_devices_api_fail(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Lunatone configuration entry not ready due to a failure in the devices API."""
|
||||
mock_lunatone_devices.async_update.side_effect = aiohttp.ClientConnectionError()
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_lunatone_info.async_update.assert_called_once()
|
||||
mock_lunatone_devices.async_update.assert_called_once()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
mock_lunatone_devices.async_update.side_effect = None
|
||||
|
||||
await hass.config_entries.async_reload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_lunatone_info.async_update.assert_called()
|
||||
mock_lunatone_devices.async_update.assert_called()
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_config_entry_not_ready_no_info_data(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Lunatone configuration entry not ready due to missing info data."""
|
||||
mock_lunatone_info.data = None
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_lunatone_info.async_update.assert_called_once()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_config_entry_not_ready_no_devices_data(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Lunatone configuration entry not ready due to missing devices data."""
|
||||
mock_lunatone_devices.data = None
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_lunatone_info.async_update.assert_called_once()
|
||||
mock_lunatone_devices.async_update.assert_called_once()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_config_entry_not_ready_no_serial_number(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Lunatone configuration entry not ready due to a missing serial number."""
|
||||
mock_lunatone_info.serial_number = None
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_lunatone_info.async_update.assert_called_once()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
79
tests/components/lunatone/test_light.py
Normal file
79
tests/components/lunatone/test_light.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Tests for the Lunatone integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_ENTITY_ID = "light.device_1"
|
||||
|
||||
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the Lunatone configuration entry loading/unloading."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entities = hass.states.async_all(Platform.LIGHT)
|
||||
for entity_state in entities:
|
||||
entity_entry = entity_registry.async_get(entity_state.entity_id)
|
||||
assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")
|
||||
assert entity_state == snapshot(name=f"{entity_entry.entity_id}-state")
|
||||
|
||||
|
||||
async def test_turn_on_off(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the light can be turned on and off."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
async def fake_update():
|
||||
device = mock_lunatone_devices.data.devices[0]
|
||||
device.features.switchable.status = not device.features.switchable.status
|
||||
|
||||
mock_lunatone_devices.async_update.side_effect = fake_update
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_OFF
|
||||
Reference in New Issue
Block a user