Add json_attributes_path configuration for command_line sensor (#116656)

Add json attributes path config to command line sensor
This commit is contained in:
atlflyer 2024-07-06 12:52:27 -04:00 committed by GitHub
parent 490dd53edf
commit ec536bda3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 74 additions and 6 deletions

View File

@ -60,12 +60,17 @@ from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.trigger_template_entity import CONF_AVAILABILITY from homeassistant.helpers.trigger_template_entity import CONF_AVAILABILITY
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN from .const import (
CONF_COMMAND_TIMEOUT,
CONF_JSON_ATTRIBUTES,
CONF_JSON_ATTRIBUTES_PATH,
DEFAULT_TIMEOUT,
DOMAIN,
)
BINARY_SENSOR_DEFAULT_NAME = "Binary Command Sensor" BINARY_SENSOR_DEFAULT_NAME = "Binary Command Sensor"
DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_OFF = "OFF"
CONF_JSON_ATTRIBUTES = "json_attributes"
SENSOR_DEFAULT_NAME = "Command Sensor" SENSOR_DEFAULT_NAME = "Command Sensor"
CONF_NOTIFIERS = "notifiers" CONF_NOTIFIERS = "notifiers"
@ -126,6 +131,7 @@ SENSOR_SCHEMA = vol.Schema(
vol.Required(CONF_COMMAND): cv.string, vol.Required(CONF_COMMAND): cv.string,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_JSON_ATTRIBUTES): cv.ensure_list_csv, vol.Optional(CONF_JSON_ATTRIBUTES): cv.ensure_list_csv,
vol.Optional(CONF_JSON_ATTRIBUTES_PATH): cv.string,
vol.Optional(CONF_NAME, default=SENSOR_DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=SENSOR_DEFAULT_NAME): cv.string,
vol.Optional(CONF_ICON): cv.template, vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,

View File

@ -18,6 +18,8 @@ from homeassistant.helpers.trigger_template_entity import (
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
CONF_COMMAND_TIMEOUT = "command_timeout" CONF_COMMAND_TIMEOUT = "command_timeout"
CONF_JSON_ATTRIBUTES = "json_attributes"
CONF_JSON_ATTRIBUTES_PATH = "json_attributes_path"
DEFAULT_TIMEOUT = 15 DEFAULT_TIMEOUT = 15
DOMAIN = "command_line" DOMAIN = "command_line"
PLATFORMS = [ PLATFORMS = [

View File

@ -3,5 +3,6 @@
"name": "Command Line", "name": "Command Line",
"codeowners": ["@gjohansson-ST"], "codeowners": ["@gjohansson-ST"],
"documentation": "https://www.home-assistant.io/integrations/command_line", "documentation": "https://www.home-assistant.io/integrations/command_line",
"iot_class": "local_polling" "iot_class": "local_polling",
"requirements": ["jsonpath==0.82.2"]
} }

View File

@ -8,6 +8,8 @@ from datetime import datetime, timedelta
import json import json
from typing import Any, cast from typing import Any, cast
from jsonpath import jsonpath
from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
@ -25,11 +27,15 @@ from homeassistant.helpers.trigger_template_entity import ManualTriggerSensorEnt
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import CONF_COMMAND_TIMEOUT, LOGGER, TRIGGER_ENTITY_OPTIONS from .const import (
CONF_COMMAND_TIMEOUT,
CONF_JSON_ATTRIBUTES,
CONF_JSON_ATTRIBUTES_PATH,
LOGGER,
TRIGGER_ENTITY_OPTIONS,
)
from .utils import async_check_output_or_log from .utils import async_check_output_or_log
CONF_JSON_ATTRIBUTES = "json_attributes"
DEFAULT_NAME = "Command Sensor" DEFAULT_NAME = "Command Sensor"
SCAN_INTERVAL = timedelta(seconds=60) SCAN_INTERVAL = timedelta(seconds=60)
@ -49,6 +55,7 @@ async def async_setup_platform(
command: str = sensor_config[CONF_COMMAND] command: str = sensor_config[CONF_COMMAND]
command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT] command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT]
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
json_attributes_path: str | None = sensor_config.get(CONF_JSON_ATTRIBUTES_PATH)
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
data = CommandSensorData(hass, command, command_timeout) data = CommandSensorData(hass, command, command_timeout)
@ -67,6 +74,7 @@ async def async_setup_platform(
trigger_entity_config, trigger_entity_config,
value_template, value_template,
json_attributes, json_attributes,
json_attributes_path,
scan_interval, scan_interval,
) )
] ]
@ -84,6 +92,7 @@ class CommandSensor(ManualTriggerSensorEntity):
config: ConfigType, config: ConfigType,
value_template: Template | None, value_template: Template | None,
json_attributes: list[str] | None, json_attributes: list[str] | None,
json_attributes_path: str | None,
scan_interval: timedelta, scan_interval: timedelta,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
@ -91,6 +100,7 @@ class CommandSensor(ManualTriggerSensorEntity):
self.data = data self.data = data
self._attr_extra_state_attributes: dict[str, Any] = {} self._attr_extra_state_attributes: dict[str, Any] = {}
self._json_attributes = json_attributes self._json_attributes = json_attributes
self._json_attributes_path = json_attributes_path
self._attr_native_value = None self._attr_native_value = None
self._value_template = value_template self._value_template = value_template
self._scan_interval = scan_interval self._scan_interval = scan_interval
@ -141,6 +151,13 @@ class CommandSensor(ManualTriggerSensorEntity):
if value: if value:
try: try:
json_dict = json.loads(value) json_dict = json.loads(value)
if self._json_attributes_path is not None:
json_dict = jsonpath(json_dict, self._json_attributes_path)
# jsonpath will always store the result in json_dict[0]
# so the next line happens to work exactly as needed to
# find the result
if isinstance(json_dict, list):
json_dict = json_dict[0]
if isinstance(json_dict, Mapping): if isinstance(json_dict, Mapping):
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {
k: json_dict[k] k: json_dict[k]

View File

@ -1187,6 +1187,7 @@ jaraco.abode==5.1.2
# homeassistant.components.jellyfin # homeassistant.components.jellyfin
jellyfin-apiclient-python==1.9.2 jellyfin-apiclient-python==1.9.2
# homeassistant.components.command_line
# homeassistant.components.rest # homeassistant.components.rest
jsonpath==0.82.2 jsonpath==0.82.2

View File

@ -974,6 +974,7 @@ jaraco.abode==5.1.2
# homeassistant.components.jellyfin # homeassistant.components.jellyfin
jellyfin-apiclient-python==1.9.2 jellyfin-apiclient-python==1.9.2
# homeassistant.components.command_line
# homeassistant.components.rest # homeassistant.components.rest
jsonpath==0.82.2 jsonpath==0.82.2

View File

@ -467,6 +467,46 @@ async def test_update_with_unnecessary_json_attrs(
assert "key_three" not in entity_state.attributes assert "key_three" not in entity_state.attributes
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"sensor": {
"name": "Test",
"command": 'echo \
{\
\\"top_level\\": {\
\\"second_level\\": {\
\\"key\\": \\"some_json_value\\",\
\\"another_key\\": \\"another_json_value\\",\
\\"key_three\\": \\"value_three\\"\
}\
}\
}',
"json_attributes": ["key", "another_key", "key_three"],
"json_attributes_path": "$.top_level.second_level",
}
}
]
}
],
)
async def test_update_with_json_attrs_with_json_attrs_path(
hass: HomeAssistant, load_yaml_integration: None
) -> None:
"""Test using json_attributes_path to select a different part of the json object as root."""
entity_state = hass.states.get("sensor.test")
assert entity_state
assert entity_state.attributes["key"] == "some_json_value"
assert entity_state.attributes["another_key"] == "another_json_value"
assert entity_state.attributes["key_three"] == "value_three"
assert "top_level" not in entity_state.attributes
assert "second_level" not in entity_state.attributes
@pytest.mark.parametrize( @pytest.mark.parametrize(
"get_config", "get_config",
[ [