Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mysensors remote platform #86376

Merged
merged 1 commit into from
Jan 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion homeassistant/components/mysensors/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class DiscoveryInfo(TypedDict):

NOTIFY_TYPES: dict[SensorType, set[ValueType]] = {"S_INFO": {"V_TEXT"}}

REMOTE_TYPES: dict[SensorType, set[ValueType]] = {"S_IR": {"V_IR_SEND"}}

SENSOR_TYPES: dict[SensorType, set[ValueType]] = {
"S_SOUND": {"V_LEVEL"},
"S_VIBRATION": {"V_LEVEL"},
Expand All @@ -107,7 +109,7 @@ class DiscoveryInfo(TypedDict):
"S_POWER": {"V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"},
"S_DISTANCE": {"V_DISTANCE"},
"S_LIGHT_LEVEL": {"V_LIGHT_LEVEL", "V_LEVEL"},
"S_IR": {"V_IR_RECEIVE"},
"S_IR": {"V_IR_RECEIVE", "V_IR_RECORD"},
"S_WATER": {"V_FLOW", "V_VOLUME"},
"S_CUSTOM": {"V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"},
"S_SCENE_CONTROLLER": {"V_SCENE_ON", "V_SCENE_OFF"},
Expand Down Expand Up @@ -144,6 +146,7 @@ class DiscoveryInfo(TypedDict):
Platform.DEVICE_TRACKER: DEVICE_TRACKER_TYPES,
Platform.LIGHT: LIGHT_TYPES,
Platform.NOTIFY: NOTIFY_TYPES,
Platform.REMOTE: REMOTE_TYPES,
Platform.SENSOR: SENSOR_TYPES,
Platform.SWITCH: SWITCH_TYPES,
Platform.TEXT: TEXT_TYPES,
Expand Down
124 changes: 124 additions & 0 deletions homeassistant/components/mysensors/remote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Support MySensors IR transceivers."""
from __future__ import annotations

from collections.abc import Iterable
from typing import Any, Optional, cast

from homeassistant.components.remote import (
ATTR_COMMAND,
RemoteEntity,
RemoteEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import setup_mysensors_platform
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo
from .device import MySensorsEntity
from .helpers import on_unload


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway)."""

@callback
def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors remote."""
setup_mysensors_platform(
hass,
Platform.REMOTE,
discovery_info,
MySensorsRemote,
async_add_entities=async_add_entities,
)

on_unload(
hass,
config_entry.entry_id,
async_dispatcher_connect(
hass,
MYSENSORS_DISCOVERY.format(config_entry.entry_id, Platform.REMOTE),
async_discover,
),
)


class MySensorsRemote(MySensorsEntity, RemoteEntity):
"""Representation of a MySensors IR transceiver."""

_current_command: str | None = None

@property
def is_on(self) -> bool | None:
"""Return True if remote is on."""
set_req = self.gateway.const.SetReq
value = cast(Optional[str], self._child.values.get(set_req.V_LIGHT))
if value is None:
return None
return value == "1"

@property
def supported_features(self) -> RemoteEntityFeature:
"""Flag supported features."""
features = RemoteEntityFeature(0)
set_req = self.gateway.const.SetReq
if set_req.V_IR_RECORD in self._values:
features = features | RemoteEntityFeature.LEARN_COMMAND
return features

async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send commands to a device."""
for cmd in command:
self._current_command = cmd
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, cmd, ack=1
)

async def async_learn_command(self, **kwargs: Any) -> None:
"""Learn a command from a device."""
set_req = self.gateway.const.SetReq
commands: list[str] | None = kwargs.get(ATTR_COMMAND)
if commands is None:
raise ValueError("Command not specified for learn_command service")

for command in commands:
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_IR_RECORD, command, ack=1
)

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the IR transceiver on."""
set_req = self.gateway.const.SetReq
if self._current_command:
self.gateway.set_child_value(
self.node_id,
self.child_id,
self.value_type,
self._current_command,
ack=1,
)
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1
)

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the IR transceiver off."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1
)

@callback
def _async_update(self) -> None:
"""Update the controller with the latest value from a device."""
super()._async_update()
self._current_command = cast(
Optional[str], self._child.values.get(self.value_type)
)
4 changes: 4 additions & 0 deletions homeassistant/components/mysensors/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
"V_IR_RECORD": SensorEntityDescription(
key="V_IR_RECORD",
icon="mdi:remote",
),
"V_PH": SensorEntityDescription(
key="V_PH",
native_unit_of_measurement="pH",
Expand Down
11 changes: 11 additions & 0 deletions homeassistant/components/mysensors/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@
}
},
"issues": {
"deprecated_entity": {
"title": "The {deprecated_entity} entity will be removed",
"fix_flow": {
"step": {
"confirm": {
"title": "The {deprecated_entity} entity will be removed",
"description": "Update any automations or scripts that use this entity in service calls using the `{deprecated_service}` service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`."
}
}
}
},
"deprecated_service": {
"title": "The {deprecated_service} service will be removed",
"fix_flow": {
Expand Down
46 changes: 45 additions & 1 deletion homeassistant/components/mysensors/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.core import HomeAssistant, ServiceCall, callback, split_entity_id
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue

from .. import mysensors
from .const import (
Expand Down Expand Up @@ -151,8 +152,35 @@ def is_on(self) -> bool:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the IR switch on."""
set_req = self.gateway.const.SetReq
placeholders = {
"deprecated_entity": self.entity_id,
"alternate_target": f"remote.{split_entity_id(self.entity_id)[1]}",
}

if ATTR_IR_CODE in kwargs:
self._ir_code = kwargs[ATTR_IR_CODE]
placeholders[
"deprecated_service"
] = f"{MYSENSORS_DOMAIN}.{SERVICE_SEND_IR_CODE}"
placeholders["alternate_service"] = "remote.send_command"
else:
placeholders["deprecated_service"] = "switch.turn_on"
placeholders["alternate_service"] = "remote.turn_on"

async_create_issue(
self.hass,
MYSENSORS_DOMAIN,
(
"deprecated_ir_switch_entity_"
f"{self.entity_id}_{placeholders['deprecated_service']}"
),
breaks_in_ha_version="2023.4.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_entity",
translation_placeholders=placeholders,
)
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, self._ir_code
)
Expand All @@ -169,6 +197,22 @@ async def async_turn_on(self, **kwargs: Any) -> None:

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the IR switch off."""
async_create_issue(
self.hass,
MYSENSORS_DOMAIN,
f"deprecated_ir_switch_entity_{self.entity_id}_switch.turn_off",
breaks_in_ha_version="2023.4.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_entity",
translation_placeholders={
"deprecated_entity": self.entity_id,
"deprecated_service": "switch.turn_off",
"alternate_service": "remote.turn_off",
"alternate_target": f"remote.{split_entity_id(self.entity_id)[1]}",
},
)
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1
Expand Down
11 changes: 11 additions & 0 deletions homeassistant/components/mysensors/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@
}
},
"issues": {
"deprecated_entity": {
"fix_flow": {
"step": {
"confirm": {
"description": "Update any automations or scripts that use this entity in service calls using the `{deprecated_service}` service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`.",
"title": "The {deprecated_entity} entity will be removed"
}
}
},
"title": "The {deprecated_entity} entity will be removed"
},
"deprecated_service": {
"fix_flow": {
"step": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"values": {
"2": "0",
"32": "test_code",
"33": "test_code"
"50": "test_code"
}
}
},
Expand Down
2 changes: 0 additions & 2 deletions tests/components/mysensors/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from __future__ import annotations

from collections.abc import Callable
from unittest.mock import MagicMock

from mysensors.sensor import Sensor

Expand All @@ -15,7 +14,6 @@ async def test_door_sensor(
hass: HomeAssistant,
door_sensor: Sensor,
receive_message: Callable[[str], None],
transport_write: MagicMock,
) -> None:
"""Test a door sensor."""
entity_id = "binary_sensor.door_sensor_1_1"
Expand Down
Loading