Skip to content

Commit 3eda687

Browse files
Smarla integration sensor platform (#145748)
1 parent 7688c36 commit 3eda687

File tree

7 files changed

+442
-1
lines changed

7 files changed

+442
-1
lines changed

homeassistant/components/smarla/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
HOST = "https://devices.swing2sleep.de"
88

9-
PLATFORMS = [Platform.NUMBER, Platform.SWITCH]
9+
PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
1010

1111
DEVICE_MODEL_NAME = "Smarla"
1212
MANUFACTURER_NAME = "Swing2Sleep"

homeassistant/components/smarla/icons.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@
99
"intensity": {
1010
"default": "mdi:sine-wave"
1111
}
12+
},
13+
"sensor": {
14+
"amplitude": {
15+
"default": "mdi:sine-wave"
16+
},
17+
"period": {
18+
"default": "mdi:sine-wave"
19+
},
20+
"activity": {
21+
"default": "mdi:baby-face"
22+
},
23+
"swing_count": {
24+
"default": "mdi:counter"
25+
}
1226
}
1327
}
1428
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Support for the Swing2Sleep Smarla sensor entities."""
2+
3+
from dataclasses import dataclass
4+
5+
from pysmarlaapi.federwiege.classes import Property
6+
7+
from homeassistant.components.sensor import (
8+
SensorEntity,
9+
SensorEntityDescription,
10+
SensorStateClass,
11+
)
12+
from homeassistant.const import UnitOfLength, UnitOfTime
13+
from homeassistant.core import HomeAssistant
14+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
15+
16+
from . import FederwiegeConfigEntry
17+
from .entity import SmarlaBaseEntity, SmarlaEntityDescription
18+
19+
20+
@dataclass(frozen=True, kw_only=True)
21+
class SmarlaSensorEntityDescription(SmarlaEntityDescription, SensorEntityDescription):
22+
"""Class describing Swing2Sleep Smarla sensor entities."""
23+
24+
multiple: bool = False
25+
value_pos: int = 0
26+
27+
28+
SENSORS: list[SmarlaSensorEntityDescription] = [
29+
SmarlaSensorEntityDescription(
30+
key="amplitude",
31+
translation_key="amplitude",
32+
service="analyser",
33+
property="oscillation",
34+
multiple=True,
35+
value_pos=0,
36+
native_unit_of_measurement=UnitOfLength.MILLIMETERS,
37+
state_class=SensorStateClass.MEASUREMENT,
38+
),
39+
SmarlaSensorEntityDescription(
40+
key="period",
41+
translation_key="period",
42+
service="analyser",
43+
property="oscillation",
44+
multiple=True,
45+
value_pos=1,
46+
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
47+
state_class=SensorStateClass.MEASUREMENT,
48+
),
49+
SmarlaSensorEntityDescription(
50+
key="activity",
51+
translation_key="activity",
52+
service="analyser",
53+
property="activity",
54+
state_class=SensorStateClass.MEASUREMENT,
55+
),
56+
SmarlaSensorEntityDescription(
57+
key="swing_count",
58+
translation_key="swing_count",
59+
service="analyser",
60+
property="swing_count",
61+
state_class=SensorStateClass.TOTAL_INCREASING,
62+
),
63+
]
64+
65+
66+
async def async_setup_entry(
67+
hass: HomeAssistant,
68+
config_entry: FederwiegeConfigEntry,
69+
async_add_entities: AddConfigEntryEntitiesCallback,
70+
) -> None:
71+
"""Set up the Smarla sensors from config entry."""
72+
federwiege = config_entry.runtime_data
73+
async_add_entities(
74+
(
75+
SmarlaSensor(federwiege, desc)
76+
if not desc.multiple
77+
else SmarlaSensorMultiple(federwiege, desc)
78+
)
79+
for desc in SENSORS
80+
)
81+
82+
83+
class SmarlaSensor(SmarlaBaseEntity, SensorEntity):
84+
"""Representation of Smarla sensor."""
85+
86+
entity_description: SmarlaSensorEntityDescription
87+
88+
_property: Property[int]
89+
90+
@property
91+
def native_value(self) -> int | None:
92+
"""Return the entity value to represent the entity state."""
93+
return self._property.get()
94+
95+
96+
class SmarlaSensorMultiple(SmarlaBaseEntity, SensorEntity):
97+
"""Representation of Smarla sensor with multiple values inside property."""
98+
99+
entity_description: SmarlaSensorEntityDescription
100+
101+
_property: Property[list[int]]
102+
103+
@property
104+
def native_value(self) -> int | None:
105+
"""Return the entity value to represent the entity state."""
106+
v = self._property.get()
107+
return v[self.entity_description.value_pos] if v is not None else None

homeassistant/components/smarla/strings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@
2828
"intensity": {
2929
"name": "Intensity"
3030
}
31+
},
32+
"sensor": {
33+
"amplitude": {
34+
"name": "Amplitude"
35+
},
36+
"period": {
37+
"name": "Period"
38+
},
39+
"activity": {
40+
"name": "Activity"
41+
},
42+
"swing_count": {
43+
"name": "Swing count",
44+
"unit_of_measurement": "swings"
45+
}
3146
}
3247
}
3348
}

tests/components/smarla/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,20 @@ def mock_federwiege(mock_connection: MagicMock) -> Generator[MagicMock]:
7373
mock_babywiege_service.props["smart_mode"].get.return_value = False
7474
mock_babywiege_service.props["intensity"].get.return_value = 1
7575

76+
mock_analyser_service = MagicMock(spec=Service)
77+
mock_analyser_service.props = {
78+
"oscillation": MagicMock(spec=Property),
79+
"activity": MagicMock(spec=Property),
80+
"swing_count": MagicMock(spec=Property),
81+
}
82+
83+
mock_analyser_service.props["oscillation"].get.return_value = [0, 0]
84+
mock_analyser_service.props["activity"].get.return_value = 0
85+
mock_analyser_service.props["swing_count"].get.return_value = 0
86+
7687
federwiege.services = {
7788
"babywiege": mock_babywiege_service,
89+
"analyser": mock_analyser_service,
7890
}
7991

8092
federwiege.get_property = MagicMock(
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# serializer version: 1
2+
# name: test_entities[sensor.smarla_activity-entry]
3+
EntityRegistryEntrySnapshot({
4+
'aliases': set({
5+
}),
6+
'area_id': None,
7+
'capabilities': dict({
8+
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
9+
}),
10+
'config_entry_id': <ANY>,
11+
'config_subentry_id': <ANY>,
12+
'device_class': None,
13+
'device_id': <ANY>,
14+
'disabled_by': None,
15+
'domain': 'sensor',
16+
'entity_category': None,
17+
'entity_id': 'sensor.smarla_activity',
18+
'has_entity_name': True,
19+
'hidden_by': None,
20+
'icon': None,
21+
'id': <ANY>,
22+
'labels': set({
23+
}),
24+
'name': None,
25+
'options': dict({
26+
}),
27+
'original_device_class': None,
28+
'original_icon': None,
29+
'original_name': 'Activity',
30+
'platform': 'smarla',
31+
'previous_unique_id': None,
32+
'suggested_object_id': None,
33+
'supported_features': 0,
34+
'translation_key': 'activity',
35+
'unique_id': 'ABCD-activity',
36+
'unit_of_measurement': None,
37+
})
38+
# ---
39+
# name: test_entities[sensor.smarla_activity-state]
40+
StateSnapshot({
41+
'attributes': ReadOnlyDict({
42+
'friendly_name': 'Smarla Activity',
43+
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
44+
}),
45+
'context': <ANY>,
46+
'entity_id': 'sensor.smarla_activity',
47+
'last_changed': <ANY>,
48+
'last_reported': <ANY>,
49+
'last_updated': <ANY>,
50+
'state': '0',
51+
})
52+
# ---
53+
# name: test_entities[sensor.smarla_amplitude-entry]
54+
EntityRegistryEntrySnapshot({
55+
'aliases': set({
56+
}),
57+
'area_id': None,
58+
'capabilities': dict({
59+
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
60+
}),
61+
'config_entry_id': <ANY>,
62+
'config_subentry_id': <ANY>,
63+
'device_class': None,
64+
'device_id': <ANY>,
65+
'disabled_by': None,
66+
'domain': 'sensor',
67+
'entity_category': None,
68+
'entity_id': 'sensor.smarla_amplitude',
69+
'has_entity_name': True,
70+
'hidden_by': None,
71+
'icon': None,
72+
'id': <ANY>,
73+
'labels': set({
74+
}),
75+
'name': None,
76+
'options': dict({
77+
}),
78+
'original_device_class': None,
79+
'original_icon': None,
80+
'original_name': 'Amplitude',
81+
'platform': 'smarla',
82+
'previous_unique_id': None,
83+
'suggested_object_id': None,
84+
'supported_features': 0,
85+
'translation_key': 'amplitude',
86+
'unique_id': 'ABCD-amplitude',
87+
'unit_of_measurement': <UnitOfLength.MILLIMETERS: 'mm'>,
88+
})
89+
# ---
90+
# name: test_entities[sensor.smarla_amplitude-state]
91+
StateSnapshot({
92+
'attributes': ReadOnlyDict({
93+
'friendly_name': 'Smarla Amplitude',
94+
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
95+
'unit_of_measurement': <UnitOfLength.MILLIMETERS: 'mm'>,
96+
}),
97+
'context': <ANY>,
98+
'entity_id': 'sensor.smarla_amplitude',
99+
'last_changed': <ANY>,
100+
'last_reported': <ANY>,
101+
'last_updated': <ANY>,
102+
'state': '0',
103+
})
104+
# ---
105+
# name: test_entities[sensor.smarla_period-entry]
106+
EntityRegistryEntrySnapshot({
107+
'aliases': set({
108+
}),
109+
'area_id': None,
110+
'capabilities': dict({
111+
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
112+
}),
113+
'config_entry_id': <ANY>,
114+
'config_subentry_id': <ANY>,
115+
'device_class': None,
116+
'device_id': <ANY>,
117+
'disabled_by': None,
118+
'domain': 'sensor',
119+
'entity_category': None,
120+
'entity_id': 'sensor.smarla_period',
121+
'has_entity_name': True,
122+
'hidden_by': None,
123+
'icon': None,
124+
'id': <ANY>,
125+
'labels': set({
126+
}),
127+
'name': None,
128+
'options': dict({
129+
}),
130+
'original_device_class': None,
131+
'original_icon': None,
132+
'original_name': 'Period',
133+
'platform': 'smarla',
134+
'previous_unique_id': None,
135+
'suggested_object_id': None,
136+
'supported_features': 0,
137+
'translation_key': 'period',
138+
'unique_id': 'ABCD-period',
139+
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
140+
})
141+
# ---
142+
# name: test_entities[sensor.smarla_period-state]
143+
StateSnapshot({
144+
'attributes': ReadOnlyDict({
145+
'friendly_name': 'Smarla Period',
146+
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
147+
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
148+
}),
149+
'context': <ANY>,
150+
'entity_id': 'sensor.smarla_period',
151+
'last_changed': <ANY>,
152+
'last_reported': <ANY>,
153+
'last_updated': <ANY>,
154+
'state': '0',
155+
})
156+
# ---
157+
# name: test_entities[sensor.smarla_swing_count-entry]
158+
EntityRegistryEntrySnapshot({
159+
'aliases': set({
160+
}),
161+
'area_id': None,
162+
'capabilities': dict({
163+
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
164+
}),
165+
'config_entry_id': <ANY>,
166+
'config_subentry_id': <ANY>,
167+
'device_class': None,
168+
'device_id': <ANY>,
169+
'disabled_by': None,
170+
'domain': 'sensor',
171+
'entity_category': None,
172+
'entity_id': 'sensor.smarla_swing_count',
173+
'has_entity_name': True,
174+
'hidden_by': None,
175+
'icon': None,
176+
'id': <ANY>,
177+
'labels': set({
178+
}),
179+
'name': None,
180+
'options': dict({
181+
}),
182+
'original_device_class': None,
183+
'original_icon': None,
184+
'original_name': 'Swing count',
185+
'platform': 'smarla',
186+
'previous_unique_id': None,
187+
'suggested_object_id': None,
188+
'supported_features': 0,
189+
'translation_key': 'swing_count',
190+
'unique_id': 'ABCD-swing_count',
191+
'unit_of_measurement': 'swings',
192+
})
193+
# ---
194+
# name: test_entities[sensor.smarla_swing_count-state]
195+
StateSnapshot({
196+
'attributes': ReadOnlyDict({
197+
'friendly_name': 'Smarla Swing count',
198+
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
199+
'unit_of_measurement': 'swings',
200+
}),
201+
'context': <ANY>,
202+
'entity_id': 'sensor.smarla_swing_count',
203+
'last_changed': <ANY>,
204+
'last_reported': <ANY>,
205+
'last_updated': <ANY>,
206+
'state': '0',
207+
})
208+
# ---

0 commit comments

Comments
 (0)