forked from home-assistant/core
Added smarla integration
This commit is contained in:
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
|
||||||
|
91
homeassistant/components/smarla/__init__.py
Normal file
91
homeassistant/components/smarla/__init__.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
"""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, ConfigEntryError
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import DOMAIN, HOST, PLATFORMS
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
type FederwiegeConfigEntry = ConfigEntry[Federwiege]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up this integration using YAML is not supported."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool:
|
||||||
|
"""Set up this integration using UI."""
|
||||||
|
if hass.data.get(DOMAIN) is None:
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
|
try:
|
||||||
|
connection = Connection(HOST, token_str=entry.data.get(CONF_ACCESS_TOKEN, None))
|
||||||
|
except ValueError as e:
|
||||||
|
raise ConfigEntryError("Invalid token") from e
|
||||||
|
|
||||||
|
if not await connection.get_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,
|
||||||
|
list(PLATFORMS),
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||||
|
entry,
|
||||||
|
list(PLATFORMS),
|
||||||
|
)
|
||||||
|
|
||||||
|
if unload_ok:
|
||||||
|
federwiege = entry.runtime_data
|
||||||
|
federwiege.disconnect()
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Reload config entry."""
|
||||||
|
await async_unload_entry(hass, entry)
|
||||||
|
await async_setup_entry(hass, entry)
|
||||||
|
|
||||||
|
|
||||||
|
class SmarlaBaseEntity(Entity):
|
||||||
|
"""Common Base Entity class for defining Smarla device."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
federwiege: Federwiege,
|
||||||
|
) -> None:
|
||||||
|
"""Initialise the entity."""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._attr_has_entity_name = True
|
||||||
|
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, federwiege.serial_number)},
|
||||||
|
name="Federwiege",
|
||||||
|
model="Smarla",
|
||||||
|
manufacturer="Swing2Sleep",
|
||||||
|
serial_number=federwiege.serial_number,
|
||||||
|
)
|
69
homeassistant/components/smarla/config_flow.py
Normal file
69
homeassistant/components/smarla/config_flow.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
"""Config flow for Swing2Sleep Smarla integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pysmarlaapi import Connection
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries, exceptions
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
|
from .const import DOMAIN, HOST
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema({CONF_ACCESS_TOKEN: str})
|
||||||
|
|
||||||
|
|
||||||
|
class SmarlaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Swing2Sleep Smarla."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None) -> config_entries.ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=STEP_USER_DATA_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = await self.validate_input(user_input)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=info["title"],
|
||||||
|
data={CONF_ACCESS_TOKEN: info.get(CONF_ACCESS_TOKEN)},
|
||||||
|
)
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except ValueError:
|
||||||
|
errors["base"] = "invalid_token"
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=STEP_USER_DATA_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def validate_input(self, data):
|
||||||
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||||
|
"""
|
||||||
|
conn = Connection(url=HOST, token_b64=data[CONF_ACCESS_TOKEN])
|
||||||
|
|
||||||
|
await self.async_set_unique_id(conn.token.serialNumber)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
if not await conn.get_token():
|
||||||
|
raise InvalidAuth
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": conn.token.serialNumber,
|
||||||
|
CONF_ACCESS_TOKEN: conn.token.get_string(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAuth(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate there is invalid auth."""
|
7
homeassistant/components/smarla/const.py
Normal file
7
homeassistant/components/smarla/const.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""Constants for the Swing2Sleep Smarla integration."""
|
||||||
|
|
||||||
|
DOMAIN = "smarla"
|
||||||
|
|
||||||
|
HOST = "https://devices.swing2sleep.de"
|
||||||
|
|
||||||
|
PLATFORMS = ["number", "sensor", "switch"]
|
28
homeassistant/components/smarla/icons.json
Normal file
28
homeassistant/components/smarla/icons.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"smartmode": {
|
||||||
|
"default": "mdi:refresh-auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"intensity": {
|
||||||
|
"default": "mdi:sine-wave"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sensor": {
|
||||||
|
"amplitude": {
|
||||||
|
"default": "mdi:sine-wave"
|
||||||
|
},
|
||||||
|
"period": {
|
||||||
|
"default": "mdi:sine-wave"
|
||||||
|
},
|
||||||
|
"activity": {
|
||||||
|
"default": "mdi:baby-face"
|
||||||
|
},
|
||||||
|
"swing_count": {
|
||||||
|
"default": "mdi:counter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
homeassistant/components/smarla/manifest.json
Normal file
13
homeassistant/components/smarla/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"domain": "smarla",
|
||||||
|
"name": "Swing2Sleep Smarla",
|
||||||
|
"codeowners": ["@explicatis", "@rlint-explicatis"],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": [],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/smarla",
|
||||||
|
"integration_type": "device",
|
||||||
|
"iot_class": "cloud_push",
|
||||||
|
"loggers": ["pysmarlaapi", "pysignalr"],
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["pysmarlaapi==0.4.0"]
|
||||||
|
}
|
93
homeassistant/components/smarla/number.py
Normal file
93
homeassistant/components/smarla/number.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
"""Support for the Swing2Sleep Smarla number entities."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from pysmarlaapi import Federwiege
|
||||||
|
|
||||||
|
from homeassistant.components.number import (
|
||||||
|
NumberEntity,
|
||||||
|
NumberEntityDescription,
|
||||||
|
NumberMode,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from . import FederwiegeConfigEntry, SmarlaBaseEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class SmarlaNumberEntityDescription(NumberEntityDescription):
|
||||||
|
"""Class describing Swing2Sleep Smarla number entities."""
|
||||||
|
|
||||||
|
service: str
|
||||||
|
property: str
|
||||||
|
|
||||||
|
|
||||||
|
NUMBER_TYPES: list[SmarlaNumberEntityDescription] = [
|
||||||
|
SmarlaNumberEntityDescription(
|
||||||
|
key="intensity",
|
||||||
|
translation_key="intensity",
|
||||||
|
service="babywiege",
|
||||||
|
property="intensity",
|
||||||
|
native_max_value=100,
|
||||||
|
native_min_value=0,
|
||||||
|
native_step=1,
|
||||||
|
mode=NumberMode.SLIDER,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: FederwiegeConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Smarla numbers from config entry."""
|
||||||
|
federwiege = config_entry.runtime_data
|
||||||
|
|
||||||
|
entities: list[SmarlaNumber] = []
|
||||||
|
|
||||||
|
for desc in NUMBER_TYPES:
|
||||||
|
entity = SmarlaNumber(federwiege, desc)
|
||||||
|
entities.append(entity)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class SmarlaNumber(SmarlaBaseEntity, NumberEntity):
|
||||||
|
"""Representation of Smarla number."""
|
||||||
|
|
||||||
|
async def on_change(self, value):
|
||||||
|
"""Notify ha when state changes."""
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
federwiege: Federwiege,
|
||||||
|
description: SmarlaNumberEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a Smarla number."""
|
||||||
|
super().__init__(federwiege)
|
||||||
|
self.property = federwiege.get_service(description.service).get_property(
|
||||||
|
description.property
|
||||||
|
)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_should_poll = False
|
||||||
|
self._attr_unique_id = f"{federwiege.serial_number}-{description.key}"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float:
|
||||||
|
"""Return the entity value to represent the entity state."""
|
||||||
|
return self.property.get()
|
||||||
|
|
||||||
|
def set_native_value(self, value: float) -> None:
|
||||||
|
"""Update to the smarla device."""
|
||||||
|
self.property.set(int(value))
|
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: todo
|
||||||
|
config-entry-unloading: todo
|
||||||
|
docs-configuration-parameters: todo
|
||||||
|
docs-installation-parameters: todo
|
||||||
|
entity-unavailable: todo
|
||||||
|
integration-owner: todo
|
||||||
|
log-when-unavailable: todo
|
||||||
|
parallel-updates: todo
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: todo
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: todo
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info: todo
|
||||||
|
discovery: todo
|
||||||
|
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: todo
|
||||||
|
repair-issues: todo
|
||||||
|
stale-devices: todo
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: todo
|
||||||
|
inject-websession: todo
|
||||||
|
strict-typing: todo
|
120
homeassistant/components/smarla/sensor.py
Normal file
120
homeassistant/components/smarla/sensor.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"""Support for the Swing2Sleep Smarla sensor entities."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from pysmarlaapi import Federwiege
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import UnitOfLength, UnitOfTime
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from . import FederwiegeConfigEntry, SmarlaBaseEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class SmarlaSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Class describing Swing2Sleep Smarla sensor entities."""
|
||||||
|
|
||||||
|
service: str
|
||||||
|
property: str
|
||||||
|
multiple: bool = False
|
||||||
|
value_pos: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
NUMBER_TYPES: list[SmarlaSensorEntityDescription] = [
|
||||||
|
SmarlaSensorEntityDescription(
|
||||||
|
key="amplitude",
|
||||||
|
translation_key="amplitude",
|
||||||
|
service="analyser",
|
||||||
|
property="oscillation",
|
||||||
|
multiple=True,
|
||||||
|
value_pos=0,
|
||||||
|
native_unit_of_measurement=UnitOfLength.MILLIMETERS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SmarlaSensorEntityDescription(
|
||||||
|
key="period",
|
||||||
|
translation_key="period",
|
||||||
|
service="analyser",
|
||||||
|
property="oscillation",
|
||||||
|
multiple=True,
|
||||||
|
value_pos=1,
|
||||||
|
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SmarlaSensorEntityDescription(
|
||||||
|
key="activity",
|
||||||
|
translation_key="activity",
|
||||||
|
service="analyser",
|
||||||
|
property="activity",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SmarlaSensorEntityDescription(
|
||||||
|
key="swing_count",
|
||||||
|
translation_key="swing_count",
|
||||||
|
service="analyser",
|
||||||
|
property="swing_count",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: FederwiegeConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Smarla sensors from config entry."""
|
||||||
|
federwiege = config_entry.runtime_data
|
||||||
|
|
||||||
|
entities: list[SmarlaSensor] = []
|
||||||
|
|
||||||
|
for desc in NUMBER_TYPES:
|
||||||
|
entity = SmarlaSensor(federwiege, desc)
|
||||||
|
entities.append(entity)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class SmarlaSensor(SmarlaBaseEntity, SensorEntity):
|
||||||
|
"""Representation of Smarla sensor."""
|
||||||
|
|
||||||
|
async def on_change(self, value):
|
||||||
|
"""Notify ha when state changes."""
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
federwiege: Federwiege,
|
||||||
|
description: SmarlaSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a Smarla sensor."""
|
||||||
|
super().__init__(federwiege)
|
||||||
|
self.property = federwiege.get_service(description.service).get_property(
|
||||||
|
description.property
|
||||||
|
)
|
||||||
|
self.entity_description = description
|
||||||
|
self.multiple = description.multiple
|
||||||
|
self.pos = description.value_pos
|
||||||
|
self._attr_should_poll = False
|
||||||
|
self._attr_unique_id = f"{federwiege.serial_number}-{description.key}"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> int:
|
||||||
|
"""Return the entity value to represent the entity state."""
|
||||||
|
return (
|
||||||
|
self.property.get() if not self.multiple else self.property.get()[self.pos]
|
||||||
|
)
|
47
homeassistant/components/smarla/strings.json
Normal file
47
homeassistant/components/smarla/strings.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"invalid_token": "Invalid access token"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"access_token": "The access token generated from the App."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"amplitude": {
|
||||||
|
"name": "Amplitude"
|
||||||
|
},
|
||||||
|
"period": {
|
||||||
|
"name": "Period"
|
||||||
|
},
|
||||||
|
"activity": {
|
||||||
|
"name": "Activity"
|
||||||
|
},
|
||||||
|
"swing_count": {
|
||||||
|
"name": "Swing count"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"intensity": {
|
||||||
|
"name": "Intensity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"smartmode": {
|
||||||
|
"name": "Smart mode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
homeassistant/components/smarla/switch.py
Normal file
96
homeassistant/components/smarla/switch.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"""Support for the Swing2Sleep Smarla switch entities."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pysmarlaapi import Federwiege
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from . import FederwiegeConfigEntry, SmarlaBaseEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class SmarlaSwitchEntityDescription(SwitchEntityDescription):
|
||||||
|
"""Class describing Swing2Sleep Smarla switch entities."""
|
||||||
|
|
||||||
|
service: str
|
||||||
|
property: str
|
||||||
|
|
||||||
|
|
||||||
|
NUMBER_TYPES: list[SmarlaSwitchEntityDescription] = [
|
||||||
|
SmarlaSwitchEntityDescription(
|
||||||
|
key="cradle",
|
||||||
|
name=None,
|
||||||
|
service="babywiege",
|
||||||
|
property="swing_active",
|
||||||
|
),
|
||||||
|
SmarlaSwitchEntityDescription(
|
||||||
|
key="smartmode",
|
||||||
|
translation_key="smartmode",
|
||||||
|
service="babywiege",
|
||||||
|
property="smartmode",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
entities: list[SmarlaSwitch] = []
|
||||||
|
|
||||||
|
for desc in NUMBER_TYPES:
|
||||||
|
entity = SmarlaSwitch(federwiege, desc)
|
||||||
|
entities.append(entity)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
|
||||||
|
"""Representation of Smarla switch."""
|
||||||
|
|
||||||
|
async def on_change(self, value):
|
||||||
|
"""Notify ha when state changes."""
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
federwiege: Federwiege,
|
||||||
|
description: SmarlaSwitchEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a Smarla switch."""
|
||||||
|
super().__init__(federwiege)
|
||||||
|
self.property = federwiege.get_service(description.service).get_property(
|
||||||
|
description.property
|
||||||
|
)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_should_poll = False
|
||||||
|
self._attr_unique_id = f"{federwiege.serial_number}-{description.key}"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@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.4.0
|
||||||
|
|
||||||
# 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.4.0
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==3.2.2
|
pysmartthings==3.2.2
|
||||||
|
|
||||||
|
1
tests/components/smarla/__init__.py
Normal file
1
tests/components/smarla/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Smarla integration."""
|
86
tests/components/smarla/test_config_flow.py
Normal file
86
tests/components/smarla/test_config_flow.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
"""Test config flow for Swing2Sleep Smarla integration."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from homeassistant.components.smarla.config_flow import Connection
|
||||||
|
from homeassistant.components.smarla.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
MOCK_ACCESS_TOKEN = "eyJyZWZyZXNoVG9rZW4iOiJ0ZXN0IiwidG9rZW4iOiJ0ZXN0IiwiZGF0ZUNyZWF0ZWQiOiIyMDI1LTAxLTAxVDIzOjU5OjU5Ljk5OTk5OVoiLCJhcHBJZGVudGlmaWVyIjoiSEEtaG9tZWFzc2lzdGFudHRlc3QiLCJzZXJpYWxOdW1iZXIiOiJBQkNELUFCQ0QiLCJhcHBWZXJzaW9uIjoidW5rbm93biIsImFwcEN1bHR1cmUiOiJkZSJ9"
|
||||||
|
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_SERIAL = "ABCD-ABCD"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_form(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that the form is shown initially."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_entry(hass: HomeAssistant) -> None:
|
||||||
|
"""Test creating a config entry."""
|
||||||
|
with patch.object(Connection, "get_token", new=AsyncMock(return_value=True)):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": "user"},
|
||||||
|
data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == MOCK_SERIAL
|
||||||
|
assert result["data"] == {CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN_JSON}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_invalid_auth(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we show user form on invalid auth."""
|
||||||
|
with patch.object(Connection, "get_token", new=AsyncMock(return_value=None)):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": "user"},
|
||||||
|
data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_invalid_token(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we handle invalid/malformed tokens."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": "user"},
|
||||||
|
data={CONF_ACCESS_TOKEN: "invalid_token"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": "invalid_token"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_exists_abort(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we abort config flow if Smarla device already configured."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id=MOCK_SERIAL,
|
||||||
|
source="user",
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": "user"},
|
||||||
|
data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
Reference in New Issue
Block a user