From 5d4216d6480deba3106d1ff083236dc030645b04 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 30 Dec 2022 00:01:03 +0100 Subject: [PATCH] Add mysensors text platform (#84667) Co-authored-by: J. Nick Koston --- homeassistant/components/mysensors/const.py | 2 + homeassistant/components/mysensors/notify.py | 25 ++++++- .../components/mysensors/strings.json | 13 ++++ homeassistant/components/mysensors/text.py | 60 +++++++++++++++++ .../components/mysensors/translations/en.json | 15 ++++- tests/components/mysensors/test_text.py | 65 +++++++++++++++++++ 6 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/mysensors/text.py create mode 100644 tests/components/mysensors/test_text.py diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 42df81ae526..03df728d62c 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -135,6 +135,7 @@ SWITCH_TYPES: dict[SensorType, set[ValueType]] = { "S_WATER_QUALITY": {"V_STATUS"}, } +TEXT_TYPES: dict[SensorType, set[ValueType]] = {"S_INFO": {"V_TEXT"}} PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = { Platform.BINARY_SENSOR: BINARY_SENSOR_TYPES, @@ -145,6 +146,7 @@ PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = { Platform.NOTIFY: NOTIFY_TYPES, Platform.SENSOR: SENSOR_TYPES, Platform.SWITCH: SWITCH_TYPES, + Platform.TEXT: TEXT_TYPES, } FLAT_PLATFORM_TYPES: dict[tuple[str, SensorType], set[ValueType]] = { diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py index b19ce7b85ca..97d4175a6f2 100644 --- a/homeassistant/components/mysensors/notify.py +++ b/homeassistant/components/mysensors/notify.py @@ -6,10 +6,12 @@ from typing import Any, cast from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import slugify from .. import mysensors -from .const import DevId, DiscoveryInfo +from .const import DOMAIN, DevId, DiscoveryInfo async def async_get_service( @@ -63,6 +65,7 @@ class MySensorsNotificationService(BaseNotificationService): ] = mysensors.get_mysensors_devices( hass, Platform.NOTIFY ) # type: ignore[assignment] + self.hass = hass async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a user.""" @@ -73,5 +76,25 @@ class MySensorsNotificationService(BaseNotificationService): if target_devices is None or device.name in target_devices ] + placeholders = { + "alternate_service": "text.set_value", + "deprecated_service": f"notify.{self._service_name}", + "alternate_target": str( + [f"text.{slugify(device.name)}" for device in devices] + ), + } + + async_create_issue( + self.hass, + DOMAIN, + "deprecated_notify_service", + breaks_in_ha_version="2023.4.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_service", + translation_placeholders=placeholders, + ) + for device in devices: device.send_msg(message) diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index dc5dc76c7ae..2bae9b08348 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -83,5 +83,18 @@ "port_out_of_range": "Port number must be at least 1 and at most 65535", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "issues": { + "deprecated_service": { + "title": "The {deprecated_service} service will be removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {deprecated_service} service will be removed", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`." + } + } + } + } } } diff --git a/homeassistant/components/mysensors/text.py b/homeassistant/components/mysensors/text.py new file mode 100644 index 00000000000..e7bb7add084 --- /dev/null +++ b/homeassistant/components/mysensors/text.py @@ -0,0 +1,60 @@ +"""Provide a text platform for MySensors.""" +from __future__ import annotations + +from homeassistant.components.text import TextEntity +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 mysensors +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 text entity.""" + mysensors.setup_mysensors_platform( + hass, + Platform.TEXT, + discovery_info, + MySensorsText, + 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.TEXT), + async_discover, + ), + ) + + +class MySensorsText(MySensorsEntity, TextEntity): + """Representation of the value of a MySensors Text child node.""" + + _attr_native_max = 25 + + @property + def native_value(self) -> str | None: + """Return the value reported by the text.""" + return self._values.get(self.value_type) + + async def async_set_value(self, value: str) -> None: + """Change the value.""" + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, value, ack=1 + ) diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index b85a28fb7d3..69e912c80ac 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -83,5 +83,18 @@ "description": "Choose connection method to the gateway" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`.", + "title": "The {deprecated_service} service will be removed" + } + } + }, + "title": "The {deprecated_service} service will be removed" + } } -} \ No newline at end of file +} diff --git a/tests/components/mysensors/test_text.py b/tests/components/mysensors/test_text.py new file mode 100644 index 00000000000..3b4b5c767d3 --- /dev/null +++ b/tests/components/mysensors/test_text.py @@ -0,0 +1,65 @@ +"""Provide tests for mysensors text platform.""" +from __future__ import annotations + +from collections.abc import Callable +from unittest.mock import MagicMock, call + +from mysensors.sensor import Sensor +import pytest + +from homeassistant.components.text import ( + ATTR_VALUE, + DOMAIN as TEXT_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + + +async def test_text_node( + hass: HomeAssistant, + text_node: Sensor, + receive_message: Callable[[str], None], + transport_write: MagicMock, +) -> None: + """Test a text node.""" + entity_id = "text.text_node_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "test" + + await hass.services.async_call( + TEXT_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: "Hello World"}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;1;47;Hello World\n") + + receive_message("1;1;1;0;47;Hello World\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "Hello World" + + transport_write.reset_mock() + + value = "12345678123456781234567812" + + with pytest.raises(ValueError) as err: + await hass.services.async_call( + TEXT_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value}, + blocking=True, + ) + + assert str(err.value) == ( + f"Value {value} for Text Node 1 1 is too long (maximum length 25)" + )