mirror of
https://github.com/home-assistant/core.git
synced 2025-11-16 14:30:22 +00:00
Compare commits
23 Commits
copilot/ad
...
smarla-int
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6f732b6a7 | ||
|
|
6e74df7d8c | ||
|
|
3ad45d4a30 | ||
|
|
00ccaf1ff9 | ||
|
|
acc32eea3e | ||
|
|
f0a3ecd27b | ||
|
|
6bbd357547 | ||
|
|
f14874bc3c | ||
|
|
73cfcc285b | ||
|
|
d5e4ed2b9e | ||
|
|
8893f97f95 | ||
|
|
2bdcdaf446 | ||
|
|
73d37972df | ||
|
|
b456af473b | ||
|
|
2fa18c6838 | ||
|
|
2a96ea0418 | ||
|
|
7b2641d136 | ||
|
|
baeb70af8a | ||
|
|
00cd8f70b9 | ||
|
|
b6d51504c4 | ||
|
|
2a2cef349d | ||
|
|
7adc1e7e47 | ||
|
|
d19aabfcd0 |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1417,6 +1417,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/sma/ @kellerza @rklomp @erwindouna
|
/tests/components/sma/ @kellerza @rklomp @erwindouna
|
||||||
/homeassistant/components/smappee/ @bsmappee
|
/homeassistant/components/smappee/ @bsmappee
|
||||||
/tests/components/smappee/ @bsmappee
|
/tests/components/smappee/ @bsmappee
|
||||||
|
/homeassistant/components/smarla/ @explicatis @rlint-explicatis
|
||||||
|
/tests/components/smarla/ @explicatis @rlint-explicatis
|
||||||
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
||||||
/tests/components/smart_meter_texas/ @grahamwetzler
|
/tests/components/smart_meter_texas/ @grahamwetzler
|
||||||
/homeassistant/components/smartthings/ @joostlek
|
/homeassistant/components/smartthings/ @joostlek
|
||||||
|
|||||||
39
homeassistant/components/smarla/__init__.py
Normal file
39
homeassistant/components/smarla/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"""The Swing2Sleep Smarla integration."""
|
||||||
|
|
||||||
|
from pysmarlaapi import Connection, Federwiege
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
|
||||||
|
from .const import HOST, PLATFORMS
|
||||||
|
|
||||||
|
type FederwiegeConfigEntry = ConfigEntry[Federwiege]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool:
|
||||||
|
"""Set up this integration using UI."""
|
||||||
|
connection = Connection(HOST, token_b64=entry.data[CONF_ACCESS_TOKEN])
|
||||||
|
|
||||||
|
# Check if token still has access
|
||||||
|
if not await connection.refresh_token():
|
||||||
|
raise ConfigEntryAuthFailed("Invalid authentication")
|
||||||
|
|
||||||
|
federwiege = Federwiege(hass.loop, connection)
|
||||||
|
federwiege.register()
|
||||||
|
federwiege.connect()
|
||||||
|
|
||||||
|
entry.runtime_data = federwiege
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
|
entry.runtime_data.disconnect()
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
62
homeassistant/components/smarla/config_flow.py
Normal file
62
homeassistant/components/smarla/config_flow.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"""Config flow for Swing2Sleep Smarla integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pysmarlaapi import Connection
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
|
from .const import DOMAIN, HOST
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema({CONF_ACCESS_TOKEN: str})
|
||||||
|
|
||||||
|
|
||||||
|
class SmarlaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Swing2Sleep Smarla."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def _handle_token(self, token: str) -> tuple[dict[str, str], str | None]:
|
||||||
|
"""Handle the token input."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = Connection(url=HOST, token_b64=token)
|
||||||
|
except ValueError:
|
||||||
|
errors["base"] = "malformed_token"
|
||||||
|
return errors, None
|
||||||
|
|
||||||
|
if not await conn.refresh_token():
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
return errors, None
|
||||||
|
|
||||||
|
return errors, conn.token.serialNumber
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
raw_token = user_input[CONF_ACCESS_TOKEN]
|
||||||
|
errors, serial_number = await self._handle_token(token=raw_token)
|
||||||
|
|
||||||
|
if not errors and serial_number is not None:
|
||||||
|
await self.async_set_unique_id(serial_number)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=serial_number,
|
||||||
|
data={CONF_ACCESS_TOKEN: raw_token},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=STEP_USER_DATA_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
12
homeassistant/components/smarla/const.py
Normal file
12
homeassistant/components/smarla/const.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"""Constants for the Swing2Sleep Smarla integration."""
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
|
DOMAIN = "smarla"
|
||||||
|
|
||||||
|
HOST = "https://devices.swing2sleep.de"
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.SWITCH]
|
||||||
|
|
||||||
|
DEVICE_MODEL_NAME = "Smarla"
|
||||||
|
MANUFACTURER_NAME = "Swing2Sleep"
|
||||||
41
homeassistant/components/smarla/entity.py
Normal file
41
homeassistant/components/smarla/entity.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""Common base for entities."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pysmarlaapi import Federwiege
|
||||||
|
from pysmarlaapi.federwiege.classes import Property
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .const import DEVICE_MODEL_NAME, DOMAIN, MANUFACTURER_NAME
|
||||||
|
|
||||||
|
|
||||||
|
class SmarlaBaseEntity(Entity):
|
||||||
|
"""Common Base Entity class for defining Smarla device."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, federwiege: Federwiege, prop: Property) -> None:
|
||||||
|
"""Initialise the entity."""
|
||||||
|
self._property = prop
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, federwiege.serial_number)},
|
||||||
|
name=DEVICE_MODEL_NAME,
|
||||||
|
model=DEVICE_MODEL_NAME,
|
||||||
|
manufacturer=MANUFACTURER_NAME,
|
||||||
|
serial_number=federwiege.serial_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_change(self, value: Any):
|
||||||
|
"""Notify ha when state changes."""
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when this Entity has been added to HA."""
|
||||||
|
await self._property.add_listener(self.on_change)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Entity being removed from hass."""
|
||||||
|
await self._property.remove_listener(self.on_change)
|
||||||
9
homeassistant/components/smarla/icons.json
Normal file
9
homeassistant/components/smarla/icons.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"smart_mode": {
|
||||||
|
"default": "mdi:refresh-auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
homeassistant/components/smarla/manifest.json
Normal file
12
homeassistant/components/smarla/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"domain": "smarla",
|
||||||
|
"name": "Swing2Sleep Smarla",
|
||||||
|
"codeowners": ["@explicatis", "@rlint-explicatis"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/smarla",
|
||||||
|
"integration_type": "device",
|
||||||
|
"iot_class": "cloud_push",
|
||||||
|
"loggers": ["pysmarlaapi", "pysignalr"],
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["pysmarlaapi==0.8.2"]
|
||||||
|
}
|
||||||
60
homeassistant/components/smarla/quality_scale.yaml
Normal file
60
homeassistant/components/smarla/quality_scale.yaml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup: done
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions: done
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup: done
|
||||||
|
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: done
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters: done
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: todo
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: todo
|
||||||
|
parallel-updates: todo
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info: todo
|
||||||
|
discovery: todo
|
||||||
|
docs-data-update: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: todo
|
||||||
|
docs-supported-devices: done
|
||||||
|
docs-supported-functions: done
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices: todo
|
||||||
|
entity-category: todo
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default: done
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: done
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues: todo
|
||||||
|
stale-devices: todo
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: todo
|
||||||
|
strict-typing: todo
|
||||||
28
homeassistant/components/smarla/strings.json
Normal file
28
homeassistant/components/smarla/strings.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"malformed_token": "Malformed access token"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"access_token": "The access token generated by the Swing2Sleep app."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"smart_mode": {
|
||||||
|
"name": "Smart Mode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
homeassistant/components/smarla/switch.py
Normal file
80
homeassistant/components/smarla/switch.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"""Support for the Swing2Sleep Smarla switch entities."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pysmarlaapi import Federwiege
|
||||||
|
from pysmarlaapi.federwiege.classes import Property
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from . import FederwiegeConfigEntry
|
||||||
|
from .entity import SmarlaBaseEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class SmarlaSwitchEntityDescription(SwitchEntityDescription):
|
||||||
|
"""Class describing Swing2Sleep Smarla switch entity."""
|
||||||
|
|
||||||
|
service: str
|
||||||
|
property: str
|
||||||
|
|
||||||
|
|
||||||
|
SWITCHES: list[SmarlaSwitchEntityDescription] = [
|
||||||
|
SmarlaSwitchEntityDescription(
|
||||||
|
key="swing_active",
|
||||||
|
name=None,
|
||||||
|
service="babywiege",
|
||||||
|
property="swing_active",
|
||||||
|
),
|
||||||
|
SmarlaSwitchEntityDescription(
|
||||||
|
key="smart_mode",
|
||||||
|
translation_key="smart_mode",
|
||||||
|
service="babywiege",
|
||||||
|
property="smart_mode",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: FederwiegeConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Smarla switches from config entry."""
|
||||||
|
federwiege = config_entry.runtime_data
|
||||||
|
async_add_entities(SmarlaSwitch(federwiege, desc) for desc in SWITCHES)
|
||||||
|
|
||||||
|
|
||||||
|
class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
|
||||||
|
"""Representation of Smarla switch."""
|
||||||
|
|
||||||
|
entity_description: SmarlaSwitchEntityDescription
|
||||||
|
|
||||||
|
_property: Property[bool]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
federwiege: Federwiege,
|
||||||
|
desc: SmarlaSwitchEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a Smarla switch."""
|
||||||
|
prop = federwiege.get_property(desc.service, desc.property)
|
||||||
|
super().__init__(federwiege, prop)
|
||||||
|
self.entity_description = desc
|
||||||
|
self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return the entity value to represent the entity state."""
|
||||||
|
return self._property.get()
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch on."""
|
||||||
|
self._property.set(True)
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch off."""
|
||||||
|
self._property.set(False)
|
||||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -577,6 +577,7 @@ FLOWS = {
|
|||||||
"slimproto",
|
"slimproto",
|
||||||
"sma",
|
"sma",
|
||||||
"smappee",
|
"smappee",
|
||||||
|
"smarla",
|
||||||
"smart_meter_texas",
|
"smart_meter_texas",
|
||||||
"smartthings",
|
"smartthings",
|
||||||
"smarttub",
|
"smarttub",
|
||||||
|
|||||||
@@ -6022,6 +6022,12 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
|
"smarla": {
|
||||||
|
"name": "Swing2Sleep Smarla",
|
||||||
|
"integration_type": "device",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_push"
|
||||||
|
},
|
||||||
"smart_blinds": {
|
"smart_blinds": {
|
||||||
"name": "Smartblinds",
|
"name": "Smartblinds",
|
||||||
"integration_type": "virtual",
|
"integration_type": "virtual",
|
||||||
|
|||||||
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@@ -2334,6 +2334,9 @@ pysma==0.7.5
|
|||||||
# homeassistant.components.smappee
|
# homeassistant.components.smappee
|
||||||
pysmappee==0.2.29
|
pysmappee==0.2.29
|
||||||
|
|
||||||
|
# homeassistant.components.smarla
|
||||||
|
pysmarlaapi==0.8.2
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==3.2.2
|
pysmartthings==3.2.2
|
||||||
|
|
||||||
|
|||||||
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@@ -1907,6 +1907,9 @@ pysma==0.7.5
|
|||||||
# homeassistant.components.smappee
|
# homeassistant.components.smappee
|
||||||
pysmappee==0.2.29
|
pysmappee==0.2.29
|
||||||
|
|
||||||
|
# homeassistant.components.smarla
|
||||||
|
pysmarlaapi==0.8.2
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==3.2.2
|
pysmartthings==3.2.2
|
||||||
|
|
||||||
|
|||||||
22
tests/components/smarla/__init__.py
Normal file
22
tests/components/smarla/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"""Tests for the Smarla integration."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||||
|
"""Set up the component."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
async def update_property_listeners(mock: AsyncMock, value: Any = None) -> None:
|
||||||
|
"""Update the property listeners for the mock object."""
|
||||||
|
for call in mock.add_listener.call_args_list:
|
||||||
|
await call[0][0](value)
|
||||||
77
tests/components/smarla/conftest.py
Normal file
77
tests/components/smarla/conftest.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"""Configuration for smarla tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from pysmarlaapi.classes import AuthToken
|
||||||
|
from pysmarlaapi.federwiege.classes import Property
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.smarla.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
|
||||||
|
from .const import MOCK_ACCESS_TOKEN_JSON, MOCK_SERIAL_NUMBER, MOCK_USER_INPUT
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Create a mock config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id=MOCK_SERIAL_NUMBER,
|
||||||
|
source=SOURCE_USER,
|
||||||
|
data=MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.smarla.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_connection() -> Generator[MagicMock]:
|
||||||
|
"""Patch Connection object."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.smarla.config_flow.Connection", autospec=True
|
||||||
|
) as mock_connection,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.smarla.Connection",
|
||||||
|
mock_connection,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
connection = mock_connection.return_value
|
||||||
|
connection.token = AuthToken.from_json(MOCK_ACCESS_TOKEN_JSON)
|
||||||
|
connection.refresh_token.return_value = True
|
||||||
|
yield connection
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_federwiege(
|
||||||
|
mock_connection: AsyncMock, mock_property: AsyncMock
|
||||||
|
) -> Generator[AsyncMock]:
|
||||||
|
"""Mock the Federwiege instance."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.smarla.Federwiege", autospec=True
|
||||||
|
) as mock_federwiege:
|
||||||
|
federwiege = mock_federwiege.return_value
|
||||||
|
federwiege.serial_number = MOCK_SERIAL_NUMBER
|
||||||
|
federwiege.get_property.return_value = mock_property
|
||||||
|
yield federwiege
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_property() -> AsyncMock:
|
||||||
|
"""Mock the Federwiege instance."""
|
||||||
|
mock = AsyncMock(spec=Property)
|
||||||
|
mock.get.return_value = False
|
||||||
|
return mock
|
||||||
20
tests/components/smarla/const.py
Normal file
20
tests/components/smarla/const.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""Constants for the Smarla integration tests."""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
|
MOCK_ACCESS_TOKEN_JSON = {
|
||||||
|
"refreshToken": "test",
|
||||||
|
"appIdentifier": "HA-test",
|
||||||
|
"serialNumber": "ABCD",
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_SERIAL_NUMBER = MOCK_ACCESS_TOKEN_JSON["serialNumber"]
|
||||||
|
|
||||||
|
MOCK_ACCESS_TOKEN = base64.b64encode(
|
||||||
|
json.dumps(MOCK_ACCESS_TOKEN_JSON).encode()
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
MOCK_USER_INPUT = {CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN}
|
||||||
95
tests/components/smarla/snapshots/test_switch.ambr
Normal file
95
tests/components/smarla/snapshots/test_switch.ambr
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_entities[switch.smarla-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'switch',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'switch.smarla',
|
||||||
|
'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': 'smarla',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': 'ABCD-swing_active',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_entities[switch.smarla-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Smarla',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'switch.smarla',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'off',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_entities[switch.smarla_smart_mode-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'switch',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'switch.smarla_smart_mode',
|
||||||
|
'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': 'Smart Mode',
|
||||||
|
'platform': 'smarla',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'smart_mode',
|
||||||
|
'unique_id': 'ABCD-smart_mode',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_entities[switch.smarla_smart_mode-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Smarla Smart Mode',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'switch.smarla_smart_mode',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'off',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
102
tests/components/smarla/test_config_flow.py
Normal file
102
tests/components/smarla/test_config_flow.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"""Test config flow for Swing2Sleep Smarla integration."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from homeassistant.components.smarla.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from .const import MOCK_SERIAL_NUMBER, MOCK_USER_INPUT
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow(
|
||||||
|
hass: HomeAssistant, mock_setup_entry, mock_connection
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a config entry."""
|
||||||
|
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"],
|
||||||
|
user_input=MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == MOCK_SERIAL_NUMBER
|
||||||
|
assert result["data"] == MOCK_USER_INPUT
|
||||||
|
assert result["result"].unique_id == MOCK_SERIAL_NUMBER
|
||||||
|
|
||||||
|
|
||||||
|
async def test_malformed_token(
|
||||||
|
hass: HomeAssistant, mock_setup_entry, mock_connection
|
||||||
|
) -> None:
|
||||||
|
"""Test we show user form on malformed token input."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.smarla.config_flow.Connection", side_effect=ValueError
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {"base": "malformed_token"}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_invalid_auth(
|
||||||
|
hass: HomeAssistant, mock_setup_entry, mock_connection
|
||||||
|
) -> None:
|
||||||
|
"""Test we show user form on invalid auth."""
|
||||||
|
with patch.object(
|
||||||
|
mock_connection, "refresh_token", new=AsyncMock(return_value=False)
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_exists_abort(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_connection
|
||||||
|
) -> None:
|
||||||
|
"""Test we abort config flow if Smarla device already configured."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
21
tests/components/smarla/test_init.py
Normal file
21
tests/components/smarla/test_init.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Test switch platform for Swing2Sleep Smarla integration."""
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_init_invalid_auth(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_connection
|
||||||
|
) -> None:
|
||||||
|
"""Test init invalid authentication behavior."""
|
||||||
|
# Add the mock entry to hass
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
mock_connection.refresh_token.return_value = False
|
||||||
|
# Set up the platform
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||||
87
tests/components/smarla/test_switch.py
Normal file
87
tests/components/smarla/test_switch.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"""Test switch platform for Swing2Sleep Smarla integration."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.switch import DOMAIN as SWITCH_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 entity_registry as er
|
||||||
|
|
||||||
|
from . import setup_integration, update_property_listeners
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, snapshot_platform
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_federwiege: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test the Spotify entities."""
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.smarla.PLATFORMS", [Platform.SWITCH]),
|
||||||
|
):
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
await snapshot_platform(
|
||||||
|
hass, entity_registry, snapshot, mock_config_entry.entry_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("service", "parameter"),
|
||||||
|
[
|
||||||
|
(SERVICE_TURN_ON, True),
|
||||||
|
(SERVICE_TURN_OFF, False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_switch_action(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_federwiege: AsyncMock,
|
||||||
|
mock_property: AsyncMock,
|
||||||
|
service: str,
|
||||||
|
parameter: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Test Smarla Switch on/off behavior."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
# Turn on
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
service,
|
||||||
|
{ATTR_ENTITY_ID: "switch.smarla"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_property.set.assert_called_once_with(parameter)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_switch_state_update(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_federwiege: AsyncMock,
|
||||||
|
mock_property: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test Smarla Switch on/off behavior."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert hass.states.get("switch.smarla").state == STATE_OFF
|
||||||
|
|
||||||
|
mock_property.get.return_value = True
|
||||||
|
|
||||||
|
await update_property_listeners(mock_property)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("switch.smarla").state == STATE_ON
|
||||||
Reference in New Issue
Block a user