@Cee,
Based on what I’ve seen, with the Xiaomi sensors, you’ll probably not get them to update using last_changed with relative times. However, with the Hue sensor you might have better luck, since it seems to have a separate last_updated attribute.
Right, the last_tripped_time attribute is specific to Envisalink. Sorry, I thought you wanted the absolute time since you had included a time_format in one of the posts above. It’s not my code originally, but I have modified it so that you can use any of the timestamp attributes discussed with relative or absolute time (see below). At some point, I’ll probably modify it take some sort of parameter to indicate how to treat the value, instead of hardcoding the attribute names. Anyway, now the presence (or lack of) time_format will control whether the time is absolute or relative.
When using last_updated with the Hue sensor, I wonder if the comma in the value will break the parsing. I tried it in the template editor and it seemed replacing it with a comma was necessary to fix it:
{{ as_timestamp("2018-09-09,08:51:48".replace(",", " ")) }}
You might need to make the corresponding edit in the attributes component where as_timestamp is used.
Relative example:
- platform: attributes
friendly_name: Last Triggered Envisalink
attribute: last_tripped_time
entities:
- binary_sensor.front_door
- binary_sensor.back_door
Absolute example:
- platform: attributes
friendly_name: Porch motion last updated
attribute: last_changed
time_format: "%m/%d %I:%M%p"
entities:
- sensor.aeotec_zw100_multisensor_6_burglar
New component code:
# """
# Creates a sensor that breaks out attribute of defined entities.
# """
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT,
ATTR_ICON, CONF_ENTITIES, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN)
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers import template as template_helper
_LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTE = "attribute"
CONF_TIME_FORMAT = "time_format"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(ATTR_ICON): cv.string,
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_TIME_FORMAT): cv.string,
vol.Required(CONF_ATTRIBUTE): cv.string,
vol.Required(CONF_ENTITIES): cv.entity_ids
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the attributes sensors."""
_LOGGER.info("Starting attribute sensor")
sensors = []
for device in config[CONF_ENTITIES]:
attr = config.get(CONF_ATTRIBUTE)
time_format = str(config.get(CONF_TIME_FORMAT))
if (attr == "last_changed"):
state_path = "states.{0}.{1}".format(device, attr)
else:
state_path = "states.{0}.attributes['{1}']".format(device, attr)
_LOGGER.info("time_format: {0}".format(time_format))
_LOGGER.info("time_format type: {0}".format(type(time_format)))
if ((attr == "last_tripped_time" or attr == "last_changed" or attr == "last_triggered" or attr == "last_updated") and time_format != None and time_format != "None"):
state_template = ("{{% if states('{0}') %}}\
{{{{ as_timestamp({1})\
| int | timestamp_custom('{2}') }}}}\
{{% else %}} {3} {{% endif %}}").format(
device, state_path, time_format, STATE_UNKNOWN)
elif attr == "battery" or attr == "battery_level":
state_template = ("{{% if states('{0}') %}}\
{{{{ {1} | float }}}}\
{{% else %}} {2} {{% endif %}}").format(
device, state_path, STATE_UNKNOWN)
elif attr == "last_tripped_time" or attr == "last_changed" or attr == "last_triggered" or attr == "last_updated":
state_template = ("{{% set time = as_timestamp({1}) | int %}}\
{{% set diff = (as_timestamp(now()) - time) | int %}}\
{{% set minutes = ((diff % 3600) / 60) | int %}}\
{{% set hours = ((diff % 86400) / 3600) | int %}}\
{{% set days = (diff / 86400) | int %}}\
{{%- if not states('{0}') or not time -%}}\
Unknown\
{{%- elif diff < 60 -%}}\
Less than a minute\
{{%- else -%}}\
{{%- if days > 0 -%}}\
{{%- if days == 1 -%}}\
1 day\
{{%- else -%}}\
{{{{ days }}}} days\
{{%- endif -%}}\
{{%- endif -%}}\
{{%- if hours > 0 -%}}\
{{%- if days > 0 -%}}\
{{{{ ', ' }}}}\
{{%- endif -%}}\
{{%- if hours == 1 -%}}\
1 hour\
{{%- else -%}}\
{{{{ hours }}}} hours\
{{%- endif -%}}\
{{%- endif -%}}\
{{%- if minutes > 0 -%}}\
{{%- if days > 0 or hours > 0 -%}}\
{{{{ ', ' }}}}\
{{%- endif -%}}\
{{%- if minutes == 1 -%}}\
1 minute\
{{%- else -%}}\
{{{{ minutes }}}} minutes\
{{%- endif -%}}\
{{%- endif -%}}\
{{%- endif -%}}").format(
device, state_path, STATE_UNKNOWN)
else:
state_template = ("{{% if states('{0}') %}}\
{{{{ {1} }}}}\
{{% else %}} {2} {{% endif %}}").format(
device, state_path, STATE_UNKNOWN)
_LOGGER.info("Adding attribute: %s of entity: %s", attr, device)
_LOGGER.debug("Applying template: %s", state_template)
state_template = template_helper.Template(state_template)
state_template.hass = hass
icon = str(config.get(ATTR_ICON))
device_state = hass.states.get(device)
if device_state is not None:
device_friendly_name = device_state.attributes.get('friendly_name')
else:
device_friendly_name = None
if device_friendly_name is None:
device_friendly_name = device.split(".", 1)[1]
friendly_name = config.get(ATTR_FRIENDLY_NAME, device_friendly_name)
unit_of_measurement = config.get(ATTR_UNIT_OF_MEASUREMENT)
if icon.startswith('mdi:'):
_LOGGER.debug("Applying user defined icon: '%s'", icon)
new_icon = ("{{% if states('{0}') %}} {1} {{% else %}}\
mdi:eye {{% endif %}}").format(device, icon)
new_icon = template_helper.Template(new_icon)
new_icon.hass = hass
elif attr == "battery" or attr == "battery_level":
_LOGGER.debug("Applying battery icon template")
new_icon = ("{{% if states('{0}') %}}\
{{% set batt = states.{0}.attributes['{1}'] %}}\
{{% if batt == 'unknown' %}}\
mdi:battery-unknown\
{{% elif batt > 95 %}}\
mdi:battery\
{{% elif batt > 85 %}}\
mdi:battery-90\
{{% elif batt > 75 %}}\
mdi:battery-80\
{{% elif batt > 65 %}}\
mdi:battery-70\
{{% elif batt > 55 %}}\
mdi:battery-60\
{{% elif batt > 45 %}}\
mdi:battery-50\
{{% elif batt > 35 %}}\
mdi:battery-40\
{{% elif batt > 25 %}}\
mdi:battery-30\
{{% elif batt > 15 %}}\
mdi:battery-20\
{{% elif batt > 10 %}}\
mdi:battery-10\
{{% else %}}\
mdi:battery-outline\
{{% endif %}}\
{{% else %}}\
mdi:battery-unknown\
{{% endif %}}").format(device, attr)
new_icon = template_helper.Template(str(new_icon))
new_icon.hass = hass
else:
_LOGGER.debug("No icon applied")
new_icon = None
sensors.append(
AttributeSensor(
hass,
("{0}_{1}").format(device.split(".", 1)[1], attr),
friendly_name,
unit_of_measurement,
state_template,
new_icon,
device)
)
if not sensors:
_LOGGER.error("No sensors added")
return False
async_add_devices(sensors)
return True
class AttributeSensor(Entity):
"""Representation of a Attribute Sensor."""
def __init__(self, hass, device_id, friendly_name, unit_of_measurement,
state_template, icon_template, entity_id):
"""Initialize the sensor."""
self.hass = hass
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id,
hass=hass)
self._name = friendly_name
self._unit_of_measurement = unit_of_measurement
self._template = state_template
self._state = None
self._icon_template = icon_template
self._icon = None
self._entity = entity_id
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
self._state = state.state
@callback
def template_sensor_state_listener(entity, old_state, new_state):
"""Handle device state changes."""
self.hass.async_add_job(self.async_update_ha_state(True))
@callback
def template_sensor_startup(event):
"""Update on startup."""
async_track_state_change(
self.hass, self._entity, template_sensor_state_listener)
self.hass.async_add_job(self.async_update_ha_state(True))
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, template_sensor_startup)
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._icon
@property
def unit_of_measurement(self):
"""Return the unit_of_measurement of the device."""
return self._unit_of_measurement
@property
def should_poll(self):
"""No polling needed."""
return False
@asyncio.coroutine
def async_update(self):
"""Update the state from the template and the friendly name."""
entity_state = self.hass.states.get(self._entity)
if entity_state is not None:
device_friendly_name = entity_state.attributes.get('friendly_name')
else:
device_friendly_name = None
if device_friendly_name is not None:
self._name = device_friendly_name
try:
self._state = self._template.async_render()
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):
# Common during HA startup - so just a warning
_LOGGER.warning('Could not render attribute sensor for %s,'
' the state is unknown.', self._entity)
return
self._state = None
_LOGGER.error('Could not attribute sensor for %s: %s',
self._entity, ex)
if self._icon_template is not None:
try:
self._icon = self._icon_template.async_render()
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):
# Common during HA startup - so just a warning
_LOGGER.warning('Could not render icon template %s,'
' the state is unknown.', self._name)
return
self._icon = super().icon
_LOGGER.error('Could not render icon template %s: %s',
self._name, ex)