HADashboard - Second sensor in widget

I had a look into doing this before, and it isn’t that easy, as AppDaemon works in a different way to HA, this is the conversion I had about it if you are interested at all.

Although we did look at doing it a slightly different way. Not sure if @ReneTode can throw anymore light on the situation at all, as I presume a template sensor will always update in AD, it might be a better solution.

by default
you can use a sensor state and a subsensor in 1 widget.
but then you wont have an icon.

in that case you need it like this:

frontdoor:
    widget_type: sensor
    title: Front Door
    entity: binary_sensor.front_door
    sub_entity: sensor.front_door_open_time

in all other cases you need to create a custom widget

Thanks! A custom widget did the trick. I’ll make a pull request if there’s interest.

1 Like

I would be interested in having a look at the custom widget if possible :+1:

no please no pull request, because Andrew wants to keep the default widgets low.
we are working on a second git were a collection from custom widgets, skins and plugins will be collected.
untill then i think its helpfull if you create your own git to share the widget with others.

i know there will be others appreciating it.

@Cee,

I put the widget here:

The formatting for the time values is a template based on this post:

And the template sensors were automatically created by customizing this custom component:

2 Likes

Many thanks for this @kodbuse

I am a little stuck getting the template sensor to work. I was wondering if you could paste your code for me.

I am currently setup like this,

- platform: attributes
  friendly_name: "Last changed"
  attribute: last_changed
  icon: 'mdi:clock'
  time_format: '%d, %m - %H:%M:%S'
  entities:
    - binary_sensor.door_window_sensor_158d00027b3daa
    - binary_sensor.door_window_sensor_158d000239432d
    - binary_sensor.door_window_sensor_158d00027b5a03
    - binary_sensor.door_window_sensor_158d000241c505
    - sensor.downstairs_motion_motion_sensor
    - sensor.hallway_motion_motion_sensor
    - binary_sensor.motion_sensor_158d0002281e2f 

But I dont seem to be getting the last changed attribute from the sensors. I also tried last_triggered too, but also not working. I think it might be my sensors not having this attribute as standard ? I am running the custom ui to get the last changed attribute to show on the frontend, but not sure how to get it into the sensor.

Could not attribute sensor for binary_sensor.motion_sensor_158d0002281e2f: UndefinedError: 'mappingproxy object' has no attribute 'last_changed'

Many thanks in advance for any help you can provide :slight_smile:

@Cee,

I was using attributes from envisalink sensors. I guess last_changed on the standard sensors has changed from an attribute to a property directly off the sensor. Making the below changes to add a “state_path” to the attributes component made it work.

  - platform: attributes
    friendly_name: Test sensor
    attribute: last_changed
    time_format: '%d, %m - %H:%M:%S'
    entities:
      - input_boolean.test_boolean
# """
# 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)        

        if (attr == "last_triggered" or
                attr == "last_changed") and time_format:

            state_template = ("{{% if states('{0}') %}}\
                              {{{{ as_timestamp({1})\
                              | int | timestamp_local()\
                              | 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":
            state_template = ("{{% set time = as_timestamp(now()) - as_timestamp({1}) %}}\
                                {{% set minutes = ((time % 3600) / 60) | int %}}\
                                {{% set hours = ((time % 86400) / 3600) | int %}}\
                                {{% set days = (time / 86400) | int %}}\
                                {{%- if time < 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)
1 Like

For some reason, template sensors using last_changed as relative time don’t update, while my template sensors using the last_tripped_time from my Envisalink sensor as relative time do. Anyone know why that would be? Is there a way to “trick” the template sensor to update on a regular basis? Absolute timestamps work for both types of sensors.

Thank you again for the work you are putting into this @kodbuse , super appreciate it.

I am using 3 Xiaomi door/window sensors, 1 of their motion sensors and 2 hue motion sensors.

I think the Xiaomi doesn’t do much else apart from open/close/battery level, they dont have many attributes in the states page.

Where as the Hue ones have a bit more information with them,

I changed the attributes component to the code above you pasted, and it seemed to sort of work, I now get the sensors populated, but with a normal date and time.

attributes%20sensors%201

attributes%20sensors%202

I apologise as I am not good at coding at all, but I from what I think I could work out, it should be the last_tripped_time ? attribute ? but when I change attributes sensor I get this

Could not attribute sensor for sensor.hallway_motion_motion_sensor: UndefinedError: 'mappingproxy object' has no attribute 'last_tripped_time'

and then nothing works again.

Kinda getting there, but also notice you were having problems with this way as well. So not sure if anything here helps you out at all.

If there is anything I can do to help out solve, just please let me know.

Many thanks again.

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

Thank you very much again @kodbuse your a superstar.

I had been trying to get something working for a while, so I think my post’s might have been a bit confusing. I am looking to get it setup exactly as you have it, your screenshot looks just like the photoshop I did up a while back as an example

with the sensor in the tile, and then the time since it was triggered underneath. I know the Xiaomi are cheap sensors, I had Hive one before, but they had a stupid two minute polling time, so were pointless for HA.

So would it be possible to maybe setup an input_boolean that would mirror the state of the door sensors, and then maybe have the attributes code run off that ? Looking at the attributes, I am going to guess probably not, just trying to think of a creative way around it.

@Cee,

This may be a bug, or perhaps it was intentionally or accidentally disabled because it was causing some sort of infinite update recursion or performance problem. I started another thread specifically about this. Hopefully we’ll get some answers:

1 Like

Awesome, thank you very much again @kodbuse for the effort you are putting into this :slight_smile: I wish I could be more helpful, but I get lost in code :frowning:

Hey!

Any chance you can share working code/instruction, tried to work with your widget from github wihout luck :
Looking how i can get motion/door since last state (i’m using xiaomi sensors) like this:
image

Thanks!

@Cee,

I got it working by explicitly tracking sensor.time in the generated attribute sensors. Here’s the new 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 -%}}\
                                    {{{{ diff }}}} seconds\
                                {{%- 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."""
            _LOGGER.info('Tracking state of %s', self._entity);
            async_track_state_change(
                self.hass, [self._entity, 'sensor.time'], 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."""

        _LOGGER.info('Updating state for %s', self._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)
1 Like

@radinsky,

My HA configuration has attribute sensors like this:

sensor:
  - platform: attributes
    friendly_name: Porch motion last updated
    attribute: last_changed
    entities:
      - sensor.aeotec_zw100_multisensor_6_burglar

And my dashboard has widgets like this:

porch:
    widget_type: sensorex
    entity: sensor.aeotec_zw100_multisensor_6_burglar
    icon_on: mdi-human
    icon_off: mdi-human
    title: Porch
    sub_entity: sensor.aeotec_zw100_multisensor_6_burglar_last_changed

Does that help?

Just had a quick play around with it, and seem’s to be working fine on the Xiaomi motion sensor, and the 2 Hue motion sensors I have, but it doesn’t seem to like the Xiaomi door sensor’s I have, it just stays at the same time.

I am a bit short of time this weekend, so probably wont have a chance to play around more till tomorrow evening.

dasah

Hey @kodbuse

Still have problems with Xioami door sensors. The motion sensor is fine, that works perfectly with the script, but I just get 0 updates from the door sensors. Dont suppose you might know why this is, or anything I can do to help out debug why they might not work ?

The motion sensor that works is at the bottom, the door sensor that doesn’t is at the top.

Many thanks in advance for any help.

@Cee

I’ve been away on vacation. So to clarify, are the values not updating, or are you not getting any valid values in the first place? I’m a bit confused since your screenshot (which looks nice, BTW), says “Closed” rather than a time value for the doors. What attribute are you using for those sensors, and what value are you seeing for those attributes if you look at them in the template editor, with something like
{{ states.binary_sensor.door_window_sensor_158d00027b5a03.last_changed }}
or
{{ states.binary_sensor.door_window_sensor_158d00027b5a03.attributes.some_other_attribute }}

1 Like