Add mysensors text platform (#84667)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Martin Hjelmare 2022-12-30 00:01:03 +01:00 committed by GitHub
parent c0da80b567
commit 5d4216d648
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 178 additions and 2 deletions

View File

@ -135,6 +135,7 @@ SWITCH_TYPES: dict[SensorType, set[ValueType]] = {
"S_WATER_QUALITY": {"V_STATUS"}, "S_WATER_QUALITY": {"V_STATUS"},
} }
TEXT_TYPES: dict[SensorType, set[ValueType]] = {"S_INFO": {"V_TEXT"}}
PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = { PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = {
Platform.BINARY_SENSOR: BINARY_SENSOR_TYPES, Platform.BINARY_SENSOR: BINARY_SENSOR_TYPES,
@ -145,6 +146,7 @@ PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = {
Platform.NOTIFY: NOTIFY_TYPES, Platform.NOTIFY: NOTIFY_TYPES,
Platform.SENSOR: SENSOR_TYPES, Platform.SENSOR: SENSOR_TYPES,
Platform.SWITCH: SWITCH_TYPES, Platform.SWITCH: SWITCH_TYPES,
Platform.TEXT: TEXT_TYPES,
} }
FLAT_PLATFORM_TYPES: dict[tuple[str, SensorType], set[ValueType]] = { FLAT_PLATFORM_TYPES: dict[tuple[str, SensorType], set[ValueType]] = {

View File

@ -6,10 +6,12 @@ from typing import Any, cast
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback 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.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from .. import mysensors from .. import mysensors
from .const import DevId, DiscoveryInfo from .const import DOMAIN, DevId, DiscoveryInfo
async def async_get_service( async def async_get_service(
@ -63,6 +65,7 @@ class MySensorsNotificationService(BaseNotificationService):
] = mysensors.get_mysensors_devices( ] = mysensors.get_mysensors_devices(
hass, Platform.NOTIFY hass, Platform.NOTIFY
) # type: ignore[assignment] ) # type: ignore[assignment]
self.hass = hass
async def async_send_message(self, message: str = "", **kwargs: Any) -> None: async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message to a user.""" """Send a message to a user."""
@ -73,5 +76,25 @@ class MySensorsNotificationService(BaseNotificationService):
if target_devices is None or device.name in target_devices 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: for device in devices:
device.send_msg(message) device.send_msg(message)

View File

@ -83,5 +83,18 @@
"port_out_of_range": "Port number must be at least 1 and at most 65535", "port_out_of_range": "Port number must be at least 1 and at most 65535",
"unknown": "[%key:common::config_flow::error::unknown%]" "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}`."
}
}
}
}
} }
} }

View File

@ -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
)

View File

@ -83,5 +83,18 @@
"description": "Choose connection method to the gateway" "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"
}
} }
} }

View File

@ -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)"
)