- Each retry was logging the error again - Now we set the cause of the PlatformNotReady to allow Home Assistant to log as needed
188 lines
6.1 KiB
Python
188 lines
6.1 KiB
Python
"""Support for RESTful API sensors."""
|
|
import json
|
|
import logging
|
|
from xml.parsers.expat import ExpatError
|
|
|
|
from jsonpath import jsonpath
|
|
import voluptuous as vol
|
|
import xmltodict
|
|
|
|
from homeassistant.components.sensor import (
|
|
DOMAIN as SENSOR_DOMAIN,
|
|
PLATFORM_SCHEMA,
|
|
SensorEntity,
|
|
)
|
|
from homeassistant.const import (
|
|
CONF_DEVICE_CLASS,
|
|
CONF_FORCE_UPDATE,
|
|
CONF_NAME,
|
|
CONF_RESOURCE,
|
|
CONF_RESOURCE_TEMPLATE,
|
|
CONF_UNIT_OF_MEASUREMENT,
|
|
CONF_VALUE_TEMPLATE,
|
|
)
|
|
from homeassistant.exceptions import PlatformNotReady
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
from . import async_get_config_and_coordinator, create_rest_data_from_config
|
|
from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH
|
|
from .entity import RestEntity
|
|
from .schema import RESOURCE_SCHEMA, SENSOR_SCHEMA
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **SENSOR_SCHEMA})
|
|
|
|
PLATFORM_SCHEMA = vol.All(
|
|
cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA
|
|
)
|
|
|
|
|
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
|
"""Set up the RESTful sensor."""
|
|
# Must update the sensor now (including fetching the rest resource) to
|
|
# ensure it's updating its state.
|
|
if discovery_info is not None:
|
|
conf, coordinator, rest = await async_get_config_and_coordinator(
|
|
hass, SENSOR_DOMAIN, discovery_info
|
|
)
|
|
else:
|
|
conf = config
|
|
coordinator = None
|
|
rest = create_rest_data_from_config(hass, conf)
|
|
await rest.async_update(log_errors=False)
|
|
|
|
if rest.data is None:
|
|
if rest.last_exception:
|
|
raise PlatformNotReady from rest.last_exception
|
|
raise PlatformNotReady
|
|
|
|
name = conf.get(CONF_NAME)
|
|
unit = conf.get(CONF_UNIT_OF_MEASUREMENT)
|
|
device_class = conf.get(CONF_DEVICE_CLASS)
|
|
json_attrs = conf.get(CONF_JSON_ATTRS)
|
|
json_attrs_path = conf.get(CONF_JSON_ATTRS_PATH)
|
|
value_template = conf.get(CONF_VALUE_TEMPLATE)
|
|
force_update = conf.get(CONF_FORCE_UPDATE)
|
|
resource_template = conf.get(CONF_RESOURCE_TEMPLATE)
|
|
|
|
if value_template is not None:
|
|
value_template.hass = hass
|
|
|
|
async_add_entities(
|
|
[
|
|
RestSensor(
|
|
coordinator,
|
|
rest,
|
|
name,
|
|
unit,
|
|
device_class,
|
|
value_template,
|
|
json_attrs,
|
|
force_update,
|
|
resource_template,
|
|
json_attrs_path,
|
|
)
|
|
],
|
|
)
|
|
|
|
|
|
class RestSensor(RestEntity, SensorEntity):
|
|
"""Implementation of a REST sensor."""
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator,
|
|
rest,
|
|
name,
|
|
unit_of_measurement,
|
|
device_class,
|
|
value_template,
|
|
json_attrs,
|
|
force_update,
|
|
resource_template,
|
|
json_attrs_path,
|
|
):
|
|
"""Initialize the REST sensor."""
|
|
super().__init__(
|
|
coordinator, rest, name, device_class, resource_template, force_update
|
|
)
|
|
self._state = None
|
|
self._unit_of_measurement = unit_of_measurement
|
|
self._value_template = value_template
|
|
self._json_attrs = json_attrs
|
|
self._attributes = None
|
|
self._json_attrs_path = json_attrs_path
|
|
|
|
@property
|
|
def unit_of_measurement(self):
|
|
"""Return the unit the value is expressed in."""
|
|
return self._unit_of_measurement
|
|
|
|
@property
|
|
def state(self):
|
|
"""Return the state of the device."""
|
|
return self._state
|
|
|
|
@property
|
|
def extra_state_attributes(self):
|
|
"""Return the state attributes."""
|
|
return self._attributes
|
|
|
|
def _update_from_rest_data(self):
|
|
"""Update state from the rest data."""
|
|
value = self.rest.data
|
|
_LOGGER.debug("Data fetched from resource: %s", value)
|
|
if self.rest.headers is not None:
|
|
# If the http request failed, headers will be None
|
|
content_type = self.rest.headers.get("content-type")
|
|
|
|
if content_type and (
|
|
content_type.startswith("text/xml")
|
|
or content_type.startswith("application/xml")
|
|
or content_type.startswith("application/xhtml+xml")
|
|
):
|
|
try:
|
|
value = json.dumps(xmltodict.parse(value))
|
|
_LOGGER.debug("JSON converted from XML: %s", value)
|
|
except ExpatError:
|
|
_LOGGER.warning(
|
|
"REST xml result could not be parsed and converted to JSON"
|
|
)
|
|
_LOGGER.debug("Erroneous XML: %s", value)
|
|
|
|
if self._json_attrs:
|
|
self._attributes = {}
|
|
if value:
|
|
try:
|
|
json_dict = json.loads(value)
|
|
if self._json_attrs_path is not None:
|
|
json_dict = jsonpath(json_dict, self._json_attrs_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, dict):
|
|
attrs = {
|
|
k: json_dict[k] for k in self._json_attrs if k in json_dict
|
|
}
|
|
self._attributes = attrs
|
|
else:
|
|
_LOGGER.warning(
|
|
"JSON result was not a dictionary"
|
|
" or list with 0th element a dictionary"
|
|
)
|
|
except ValueError:
|
|
_LOGGER.warning("REST result could not be parsed as JSON")
|
|
_LOGGER.debug("Erroneous JSON: %s", value)
|
|
|
|
else:
|
|
_LOGGER.warning("Empty reply found when expecting JSON data")
|
|
|
|
if value is not None and self._value_template is not None:
|
|
value = self._value_template.async_render_with_possible_json_value(
|
|
value, None
|
|
)
|
|
|
|
self._state = value
|