Add Netatmo fan platform (#107989)
* Add fan platform to support NLLF centralized ventilation devices * Update tests/components/netatmo/test_fan.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update tests/components/netatmo/test_fan.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update tests/components/netatmo/test_fan.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update tests/components/netatmo/test_fan.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * add snapshots * update snapshot * fix docstring * address comment --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
acbc2350d0
commit
f808c2ff14
13 changed files with 356 additions and 6 deletions
|
@ -42,6 +42,7 @@ NETATMO_CREATE_CAMERA = "netatmo_create_camera"
|
|||
NETATMO_CREATE_CAMERA_LIGHT = "netatmo_create_camera_light"
|
||||
NETATMO_CREATE_CLIMATE = "netatmo_create_climate"
|
||||
NETATMO_CREATE_COVER = "netatmo_create_cover"
|
||||
NETATMO_CREATE_FAN = "netatmo_create_fan"
|
||||
NETATMO_CREATE_LIGHT = "netatmo_create_light"
|
||||
NETATMO_CREATE_ROOM_SENSOR = "netatmo_create_room_sensor"
|
||||
NETATMO_CREATE_SELECT = "netatmo_create_select"
|
||||
|
|
|
@ -37,6 +37,7 @@ from .const import (
|
|||
NETATMO_CREATE_CAMERA_LIGHT,
|
||||
NETATMO_CREATE_CLIMATE,
|
||||
NETATMO_CREATE_COVER,
|
||||
NETATMO_CREATE_FAN,
|
||||
NETATMO_CREATE_LIGHT,
|
||||
NETATMO_CREATE_ROOM_SENSOR,
|
||||
NETATMO_CREATE_SELECT,
|
||||
|
@ -356,6 +357,7 @@ class NetatmoDataHandler:
|
|||
NETATMO_CREATE_SENSOR,
|
||||
],
|
||||
NetatmoDeviceCategory.meter: [NETATMO_CREATE_SENSOR],
|
||||
NetatmoDeviceCategory.fan: [NETATMO_CREATE_FAN],
|
||||
}
|
||||
for module in home.modules.values():
|
||||
if not module.device_category:
|
||||
|
|
87
homeassistant/components/netatmo/fan.py
Normal file
87
homeassistant/components/netatmo/fan.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
"""Support for Netatmo/Bubendorff fans."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Final, cast
|
||||
|
||||
from pyatmo import modules as NaModules
|
||||
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import CONF_URL_CONTROL, NETATMO_CREATE_FAN
|
||||
from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice
|
||||
from .entity import NetatmoBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PERCENTAGE: Final = 50
|
||||
|
||||
PRESET_MAPPING = {"slow": 1, "fast": 2}
|
||||
PRESETS = {v: k for k, v in PRESET_MAPPING.items()}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Netatmo fan platform."""
|
||||
|
||||
@callback
|
||||
def _create_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
entity = NetatmoFan(netatmo_device)
|
||||
_LOGGER.debug("Adding cover %s", entity)
|
||||
async_add_entities([entity])
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, NETATMO_CREATE_FAN, _create_entity)
|
||||
)
|
||||
|
||||
|
||||
class NetatmoFan(NetatmoBaseEntity, FanEntity):
|
||||
"""Representation of a Netatmo fan."""
|
||||
|
||||
_attr_preset_modes = ["slow", "fast"]
|
||||
_attr_supported_features = FanEntityFeature.PRESET_MODE
|
||||
|
||||
def __init__(self, netatmo_device: NetatmoDevice) -> None:
|
||||
"""Initialize of Netatmo fan."""
|
||||
super().__init__(netatmo_device.data_handler)
|
||||
|
||||
self._fan = cast(NaModules.Fan, netatmo_device.device)
|
||||
|
||||
self._id = self._fan.entity_id
|
||||
self._attr_name = self._device_name = self._fan.name
|
||||
self._model = self._fan.device_type
|
||||
self._config_url = CONF_URL_CONTROL
|
||||
|
||||
self._home_id = self._fan.home.entity_id
|
||||
|
||||
self._signal_name = f"{HOME}-{self._home_id}"
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": HOME,
|
||||
"home_id": self._home_id,
|
||||
SIGNAL_NAME: self._signal_name,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
self._attr_unique_id = f"{self._id}-{self._model}"
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
await self._fan.async_set_fan_speed(PRESET_MAPPING[preset_mode])
|
||||
|
||||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
if self._fan.fan_speed is None:
|
||||
self._attr_preset_mode = None
|
||||
return
|
||||
self._attr_preset_mode = PRESETS.get(self._fan.fan_speed)
|
|
@ -12,5 +12,5 @@
|
|||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyatmo"],
|
||||
"requirements": ["pyatmo==8.0.2"]
|
||||
"requirements": ["pyatmo==8.0.3"]
|
||||
}
|
||||
|
|
|
@ -1654,7 +1654,7 @@ pyasuswrt==0.1.21
|
|||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==8.0.2
|
||||
pyatmo==8.0.3
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.14.3
|
||||
|
|
|
@ -1274,7 +1274,7 @@ pyasuswrt==0.1.21
|
|||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==8.0.2
|
||||
pyatmo==8.0.3
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.14.3
|
||||
|
|
|
@ -23,7 +23,8 @@
|
|||
"12:34:56:00:f1:62",
|
||||
"12:34:56:10:f1:66",
|
||||
"12:34:56:00:e3:9b",
|
||||
"0009999992"
|
||||
"0009999992",
|
||||
"0009999993"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -174,7 +175,7 @@
|
|||
"name": "module iDiamant",
|
||||
"setup_date": 1562262465,
|
||||
"room_id": "222452125",
|
||||
"modules_bridged": ["0009999992"]
|
||||
"modules_bridged": ["0009999992", "0009999993"]
|
||||
},
|
||||
{
|
||||
"id": "0009999992",
|
||||
|
@ -184,6 +185,14 @@
|
|||
"room_id": "3688132631",
|
||||
"bridge": "12:34:56:30:d5:d4"
|
||||
},
|
||||
{
|
||||
"id": "0009999993",
|
||||
"type": "NBO",
|
||||
"name": "Bubendorff blind",
|
||||
"setup_date": 1594132017,
|
||||
"room_id": "3688132631",
|
||||
"bridge": "12:34:56:30:d5:d4"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:bb:26",
|
||||
"type": "NAMain",
|
||||
|
@ -310,7 +319,8 @@
|
|||
"12:34:56:80:00:c3:69:3c",
|
||||
"12:34:56:00:00:a1:4c:da",
|
||||
"12:34:56:00:01:01:01:a1",
|
||||
"00:11:22:33:00:11:45:fe"
|
||||
"00:11:22:33:00:11:45:fe",
|
||||
"12:34:56:00:01:01:01:b1"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -466,6 +476,14 @@
|
|||
"setup_date": 1598367404,
|
||||
"room_id": "1002003001",
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:01:01:01:b1",
|
||||
"type": "NLLF",
|
||||
"name": "Centralized ventilation controler",
|
||||
"setup_date": 1598367504,
|
||||
"room_id": "1002003001",
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
}
|
||||
],
|
||||
"schedules": [
|
||||
|
|
|
@ -139,6 +139,18 @@
|
|||
"reachable": true,
|
||||
"bridge": "12:34:56:30:d5:d4"
|
||||
},
|
||||
{
|
||||
"id": "0009999993",
|
||||
"type": "NBO",
|
||||
"current_position": 0,
|
||||
"target_position": 0,
|
||||
"target_position:step": 100,
|
||||
"firmware_revision": 22,
|
||||
"rf_strength": 0,
|
||||
"last_seen": 1671395511,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:30:d5:d4"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:86:99",
|
||||
"type": "NACamDoorTag",
|
||||
|
@ -276,6 +288,16 @@
|
|||
"power": 0,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:01:01:01:b1",
|
||||
"type": "NLLF",
|
||||
"firmware_revision": 60,
|
||||
"last_seen": 1657086949,
|
||||
"power": 11,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:80:60:40",
|
||||
"fan_speed": 1
|
||||
}
|
||||
],
|
||||
"rooms": [
|
||||
|
|
|
@ -1,4 +1,51 @@
|
|||
# serializer version: 1
|
||||
# name: test_entity[cover.bubendorff_blind-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'cover',
|
||||
'entity_category': None,
|
||||
'entity_id': 'cover.bubendorff_blind',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <CoverDeviceClass.SHUTTER: 'shutter'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Bubendorff blind',
|
||||
'platform': 'netatmo',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
'translation_key': None,
|
||||
'unique_id': '0009999993-DeviceType.NBO',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entity[cover.bubendorff_blind-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by Netatmo',
|
||||
'current_position': 0,
|
||||
'device_class': 'shutter',
|
||||
'friendly_name': 'Bubendorff blind',
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'cover.bubendorff_blind',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'closed',
|
||||
})
|
||||
# ---
|
||||
# name: test_entity[cover.entrance_blinds-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
|
|
@ -111,6 +111,7 @@
|
|||
'id': '12:34:56:30:d5:d4',
|
||||
'modules_bridged': list([
|
||||
'0009999992',
|
||||
'0009999993',
|
||||
]),
|
||||
'name': '**REDACTED**',
|
||||
'room_id': '222452125',
|
||||
|
@ -125,6 +126,14 @@
|
|||
'setup_date': 1578551339,
|
||||
'type': 'NBR',
|
||||
}),
|
||||
dict({
|
||||
'bridge': '12:34:56:30:d5:d4',
|
||||
'id': '0009999993',
|
||||
'name': '**REDACTED**',
|
||||
'room_id': '3688132631',
|
||||
'setup_date': 1594132017,
|
||||
'type': 'NBO',
|
||||
}),
|
||||
dict({
|
||||
'alarm_config': dict({
|
||||
'default_alarm': list([
|
||||
|
@ -248,6 +257,7 @@
|
|||
'12:34:56:00:00:a1:4c:da',
|
||||
'12:34:56:00:01:01:01:a1',
|
||||
'00:11:22:33:00:11:45:fe',
|
||||
'12:34:56:00:01:01:01:b1',
|
||||
]),
|
||||
'name': '**REDACTED**',
|
||||
'room_id': '1310352496',
|
||||
|
@ -408,6 +418,14 @@
|
|||
'setup_date': 1598367404,
|
||||
'type': 'NLFN',
|
||||
}),
|
||||
dict({
|
||||
'bridge': '12:34:56:80:60:40',
|
||||
'id': '12:34:56:00:01:01:01:b1',
|
||||
'name': '**REDACTED**',
|
||||
'room_id': '1002003001',
|
||||
'setup_date': 1598367504,
|
||||
'type': 'NLLF',
|
||||
}),
|
||||
]),
|
||||
'name': '**REDACTED**',
|
||||
'persons': list([
|
||||
|
@ -443,6 +461,7 @@
|
|||
'12:34:56:10:f1:66',
|
||||
'12:34:56:00:e3:9b',
|
||||
'0009999992',
|
||||
'0009999993',
|
||||
]),
|
||||
'name': '**REDACTED**',
|
||||
'type': 'custom',
|
||||
|
|
56
tests/components/netatmo/snapshots/test_fan.ambr
Normal file
56
tests/components/netatmo/snapshots/test_fan.ambr
Normal file
|
@ -0,0 +1,56 @@
|
|||
# serializer version: 1
|
||||
# name: test_entity[fan.centralized_ventilation_controler-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'preset_modes': list([
|
||||
'slow',
|
||||
'fast',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'fan',
|
||||
'entity_category': None,
|
||||
'entity_id': 'fan.centralized_ventilation_controler',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Centralized ventilation controler',
|
||||
'platform': 'netatmo',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <FanEntityFeature: 8>,
|
||||
'translation_key': None,
|
||||
'unique_id': '12:34:56:00:01:01:01:b1-DeviceType.NLLF',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entity[fan.centralized_ventilation_controler-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by Netatmo',
|
||||
'friendly_name': 'Centralized ventilation controler',
|
||||
'preset_mode': 'slow',
|
||||
'preset_modes': list([
|
||||
'slow',
|
||||
'fast',
|
||||
]),
|
||||
'supported_features': <FanEntityFeature: 8>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.centralized_ventilation_controler',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
|
@ -27,6 +27,34 @@
|
|||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[netatmo-0009999993]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': 'https://home.netatmo.com/control',
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'netatmo',
|
||||
'0009999993',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'manufacturer': 'Bubbendorf',
|
||||
'model': 'Orientable Shutter',
|
||||
'name': 'Bubendorff blind',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[netatmo-00:11:22:33:00:11:45:fe]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
|
|
70
tests/components/netatmo/test_fan.py
Normal file
70
tests/components/netatmo/test_fan.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
"""The tests for Netatmo fan."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
ATTR_PRESET_MODE,
|
||||
DOMAIN as FAN_DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from .common import selected_platforms, snapshot_platform_entities
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_entity(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
netatmo_auth: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test entities."""
|
||||
await snapshot_platform_entities(
|
||||
hass,
|
||||
config_entry,
|
||||
Platform.FAN,
|
||||
entity_registry,
|
||||
snapshot,
|
||||
)
|
||||
|
||||
|
||||
async def test_switch_setup_and_services(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, netatmo_auth: AsyncMock
|
||||
) -> None:
|
||||
"""Test setup and services."""
|
||||
with selected_platforms([Platform.FAN]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
fan_entity = "fan.centralized_ventilation_controler"
|
||||
|
||||
assert hass.states.get(fan_entity).state == "on"
|
||||
assert hass.states.get(fan_entity).attributes[ATTR_PRESET_MODE] == "slow"
|
||||
|
||||
# Test turning switch on
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{ATTR_ENTITY_ID: fan_entity, ATTR_PRESET_MODE: "fast"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "12:34:56:00:01:01:01:b1",
|
||||
"fan_speed": 2,
|
||||
"bridge": "12:34:56:80:60:40",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
Loading…
Add table
Reference in a new issue