Code refactoring

This commit is contained in:
Robin Lintermann 2025-04-23 14:41:44 +00:00
parent baeb70af8a
commit 7b2641d136
6 changed files with 44 additions and 38 deletions

View File

@ -1,7 +1,12 @@
"""Constants for the Swing2Sleep Smarla integration.""" """Constants for the Swing2Sleep Smarla integration."""
from homeassistant.const import Platform
DOMAIN = "smarla" DOMAIN = "smarla"
HOST = "https://devices.swing2sleep.de" HOST = "https://devices.swing2sleep.de"
PLATFORMS = ["switch"] PLATFORMS = [Platform.SWITCH]
DEVICE_MODEL_NAME = "Smarla"
MANUFACTURER_NAME = "Swing2Sleep"

View File

@ -5,12 +5,14 @@ from pysmarlaapi import Federwiege
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import DOMAIN from .const import DEVICE_MODEL_NAME, DOMAIN, MANUFACTURER_NAME
class SmarlaBaseEntity(Entity): class SmarlaBaseEntity(Entity):
"""Common Base Entity class for defining Smarla device.""" """Common Base Entity class for defining Smarla device."""
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
federwiege: Federwiege, federwiege: Federwiege,
@ -18,12 +20,10 @@ class SmarlaBaseEntity(Entity):
"""Initialise the entity.""" """Initialise the entity."""
super().__init__() super().__init__()
self._attr_has_entity_name = True
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, federwiege.serial_number)}, identifiers={(DOMAIN, federwiege.serial_number)},
name="Smarla", name=DEVICE_MODEL_NAME,
model="Smarla", model=DEVICE_MODEL_NAME,
manufacturer="Swing2Sleep", manufacturer=MANUFACTURER_NAME,
serial_number=federwiege.serial_number, serial_number=federwiege.serial_number,
) )

View File

@ -3,7 +3,6 @@
"name": "Swing2Sleep Smarla", "name": "Swing2Sleep Smarla",
"codeowners": ["@explicatis", "@rlint-explicatis"], "codeowners": ["@explicatis", "@rlint-explicatis"],
"config_flow": true, "config_flow": true,
"dependencies": [],
"documentation": "https://www.home-assistant.io/integrations/smarla", "documentation": "https://www.home-assistant.io/integrations/smarla",
"integration_type": "device", "integration_type": "device",
"iot_class": "cloud_push", "iot_class": "cloud_push",

View File

@ -10,7 +10,7 @@
"access_token": "[%key:common::config_flow::data::access_token%]" "access_token": "[%key:common::config_flow::data::access_token%]"
}, },
"data_description": { "data_description": {
"access_token": "The access token generated from the App." "access_token": "The access token generated by the Swing2Sleep app."
} }
} }
}, },

View File

@ -4,6 +4,7 @@ from dataclasses import dataclass
from typing import Any from typing import Any
from pysmarlaapi import Federwiege from pysmarlaapi import Federwiege
from pysmarlaapi.federwiege.classes import Property
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -15,13 +16,13 @@ from .entity import SmarlaBaseEntity
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class SmarlaSwitchEntityDescription(SwitchEntityDescription): class SmarlaSwitchEntityDescription(SwitchEntityDescription):
"""Class describing Swing2Sleep Smarla switch entities.""" """Class describing Swing2Sleep Smarla switch entity."""
service: str service: str
property: str property: str
NUMBER_TYPES: list[SmarlaSwitchEntityDescription] = [ SWITCHES: list[SmarlaSwitchEntityDescription] = [
SmarlaSwitchEntityDescription( SmarlaSwitchEntityDescription(
key="cradle", key="cradle",
name=None, name=None,
@ -43,20 +44,17 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up the Smarla switches from config entry.""" """Set up the Smarla switches from config entry."""
federwiege = config_entry.runtime_data federwiege: Federwiege = config_entry.runtime_data
async_add_entities(SmarlaSwitch(federwiege, desc) for desc in SWITCHES)
entities: list[SwitchEntity] = []
for desc in NUMBER_TYPES:
entity = SmarlaSwitch(federwiege, desc)
entities.append(entity)
async_add_entities(entities)
class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity): class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
"""Representation of Smarla switch.""" """Representation of Smarla switch."""
entity_description: SmarlaSwitchEntityDescription
_property: Property
_attr_should_poll = False
async def on_change(self, value: Any): async def on_change(self, value: Any):
"""Notify ha when state changes.""" """Notify ha when state changes."""
self.async_write_ha_state() self.async_write_ha_state()
@ -68,30 +66,29 @@ class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
) -> None: ) -> None:
"""Initialize a Smarla switch.""" """Initialize a Smarla switch."""
super().__init__(federwiege) super().__init__(federwiege)
self.property = federwiege.get_service(description.service).get_property( self._property = federwiege.get_service(description.service).get_property(
description.property description.property
) )
self.entity_description = description self.entity_description = description
self._attr_should_poll = False
self._attr_unique_id = f"{federwiege.serial_number}-{description.key}" self._attr_unique_id = f"{federwiege.serial_number}-{description.key}"
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when this Entity has been added to HA.""" """Run when this Entity has been added to HA."""
await self.property.add_listener(self.on_change) await self._property.add_listener(self.on_change)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Entity being removed from hass.""" """Entity being removed from hass."""
await self.property.remove_listener(self.on_change) await self._property.remove_listener(self.on_change)
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the entity value to represent the entity state.""" """Return the entity value to represent the entity state."""
return self.property.get() return self._property.get()
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on.""" """Turn the switch on."""
self.property.set(True) self._property.set(True)
def turn_off(self, **kwargs: Any) -> None: def turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off.""" """Turn the switch off."""
self.property.set(False) self._property.set(False)

View File

@ -1,7 +1,9 @@
"""Test config flow for Swing2Sleep Smarla integration.""" """Test config flow for Swing2Sleep Smarla integration."""
import json
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from homeassistant import config_entries
from homeassistant.components.smarla.config_flow import Connection from homeassistant.components.smarla.config_flow import Connection
from homeassistant.components.smarla.const import DOMAIN from homeassistant.components.smarla.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
@ -10,9 +12,12 @@ from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
MOCK_ACCESS_TOKEN = "eyJyZWZyZXNoVG9rZW4iOiJ0ZXN0IiwidG9rZW4iOiJ0ZXN0IiwiZGF0ZUNyZWF0ZWQiOiIyMDI1LTAxLTAxVDIzOjU5OjU5Ljk5OTk5OVoiLCJhcHBJZGVudGlmaWVyIjoiSEEtaG9tZWFzc2lzdGFudHRlc3QiLCJzZXJpYWxOdW1iZXIiOiJBQkNELUFCQ0QiLCJhcHBWZXJzaW9uIjoidW5rbm93biIsImFwcEN1bHR1cmUiOiJkZSJ9" MOCK_ACCESS_TOKEN = "eyJyZWZyZXNoVG9rZW4iOiJ0ZXN0IiwiYXBwSWRlbnRpZmllciI6IkhBLXRlc3QiLCJzZXJpYWxOdW1iZXIiOiJBQkNEIn0="
MOCK_ACCESS_TOKEN_JSON = '{"refreshToken": "test", "token": "test", "dateCreated": "2025-01-01T23:59:59.999999Z", "appIdentifier": "HA-homeassistanttest", "serialNumber": "ABCD-ABCD", "appVersion": "unknown", "appCulture": "de"}' MOCK_ACCESS_TOKEN_JSON = {
MOCK_SERIAL = "ABCD-ABCD" "refreshToken": "test",
"appIdentifier": "HA-test",
"serialNumber": "ABCD",
}
async def test_show_form(hass: HomeAssistant) -> None: async def test_show_form(hass: HomeAssistant) -> None:
@ -30,13 +35,13 @@ async def test_create_entry(hass: HomeAssistant) -> None:
with patch.object(Connection, "get_token", new=AsyncMock(return_value=True)): with patch.object(Connection, "get_token", new=AsyncMock(return_value=True)):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": "user"}, context={"source": config_entries.SOURCE_USER},
data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN}, data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN},
) )
assert result["type"] == FlowResultType.CREATE_ENTRY assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == MOCK_SERIAL assert result["title"] == MOCK_ACCESS_TOKEN_JSON["serialNumber"]
assert result["data"] == {CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN_JSON} assert result["data"] == {CONF_ACCESS_TOKEN: json.dumps(MOCK_ACCESS_TOKEN_JSON)}
async def test_invalid_auth(hass: HomeAssistant) -> None: async def test_invalid_auth(hass: HomeAssistant) -> None:
@ -44,7 +49,7 @@ async def test_invalid_auth(hass: HomeAssistant) -> None:
with patch.object(Connection, "get_token", new=AsyncMock(return_value=None)): with patch.object(Connection, "get_token", new=AsyncMock(return_value=None)):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": "user"}, context={"source": config_entries.SOURCE_USER},
data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN}, data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN},
) )
@ -56,7 +61,7 @@ async def test_invalid_token(hass: HomeAssistant) -> None:
"""Test we handle invalid/malformed tokens.""" """Test we handle invalid/malformed tokens."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": "user"}, context={"source": config_entries.SOURCE_USER},
data={CONF_ACCESS_TOKEN: "invalid_token"}, data={CONF_ACCESS_TOKEN: "invalid_token"},
) )
@ -68,8 +73,8 @@ async def test_device_exists_abort(hass: HomeAssistant) -> None:
"""Test we abort config flow if Smarla device already configured.""" """Test we abort config flow if Smarla device already configured."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
unique_id=MOCK_SERIAL, unique_id=MOCK_ACCESS_TOKEN_JSON["serialNumber"],
source="user", source=config_entries.SOURCE_USER,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -77,7 +82,7 @@ async def test_device_exists_abort(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": "user"}, context={"source": config_entries.SOURCE_USER},
data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN}, data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN},
) )