Time-based numeric counter custom component

Hi,

I was really unhappy with HomeAssistant not having time-based counter. Like one where I can set MIN and MAX values and time DURATION that needs to pass between MIN and MAX. Also the value is the key so it has to store (and restore) state of the counter.

In my opinion using trigger platform: time_pattern; seconds: /1 with timer or counter was not the best way to accomplish it.

I ended up writing my own custom component named time_counter.

Configuration reference:

time_counter:
  my_counter:
    name: string
    duration: int, required [s]
    initial: int, defaults to 0
    min: int, defaults to 0
    max: int, defaults to 100
    debounce: int, optional, defaults to 1000 [ms]

You can also set icon and restore as documented in counter component.

Available services are:

  • time_counter.upcount ā€“ starts counting towards MAX.
  • time_counter.downcount ā€“ starts counting towards MIN.
  • time_counter.set ā€“ starts counting towards state: int value given in data for service.
  • time_counter.stop ā€“ stops counting immediately.

There are two events:

  • time_counter.started ā€“ fired when counter starts counting.
  • time_counter.stopped ā€“ fired when counter stopped counting.
    This is:
    a) When timer reached boundaries (MIN or MAX).
    b) When timer reached state given in time_counter.set service call.

You can also get state of counter via event.state.

How it works:

The idea of component is this: we start at INITIAL and store current state between MIN and MAX as entity state. Using downcount or upcount we start changing current state towards MIN or MAX in given part of time DURATION to reflect real device behind this timer.

You can use this component to control devices that cannot provide state informations, but you know the time between MIN and MAX, so you can change states on your own.

An example can be two 230V switch covers: switch-1 opens the cover, switch-2 closes the cover. We donā€™t know the moment in which cover is in particular state, but we know the time. So we can use it now.

time_counter.py component:

"""Time-based counter."""
from datetime import timedelta
import logging

import voluptuous as vol

from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_point_in_utc_time, async_track_time_interval
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'time_counter'
ENTITY_ID_FORMAT = DOMAIN + '.{}'

ATTR_STATE = 'state'

CONF_RESTORE = 'restore'

ATTR_DURATION = 'duration'
CONF_DURATION = 'duration'

DEFAULT_INITIAL = 0
ATTR_INITIAL = 'initial'
CONF_INITIAL = 'initial'

DEFAULT_MIN = 0
ATTR_MIN = 'min'
CONF_MIN = 'min'

DEFAULT_MAX = 100
ATTR_MAX = 'max'
CONF_MAX = 'max'

DEFAULT_DEBOUNCE = 1000
ATTR_DEBOUNCE = 'debounce'
CONF_DEBOUNCE = 'debounce'

MODE_UPCOUNTING = 1
MODE_DOWNCOUNTING = -1

EVENT_TIME_COUNTER_STARTED = 'time_counter.started'
EVENT_TIME_COUNTER_STOPPED = 'time_counter.stopped'

SERVICE_UPCOUNT = 'upcount'
SERVICE_DOWNCOUNT = 'downcount'
SERVICE_STOP = 'stop'
SERVICE_SET = 'set'

SERVICE_SCHEMA = vol.Schema({
    vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
})

SERVICE_SCHEMA_SET = vol.Schema({
    vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
    vol.Required(ATTR_STATE): cv.positive_int,
})

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: cv.schema_with_slug_keys(
        vol.Any({
            vol.Optional(CONF_NAME): cv.string,
            vol.Optional(CONF_ICON): cv.icon,
            vol.Optional(CONF_RESTORE, default=True): cv.boolean,
            vol.Required(CONF_DURATION):
                cv.positive_int,
            vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL):
                cv.positive_int,
            vol.Optional(CONF_MIN, default=DEFAULT_MIN):
                cv.positive_int,
            vol.Optional(CONF_MAX, default=DEFAULT_MAX):
                cv.positive_int,
            vol.Optional(CONF_DEBOUNCE, default=DEFAULT_DEBOUNCE):
                cv.positive_int,
        }, None)
    )
}, extra=vol.ALLOW_EXTRA)

async def async_setup(hass, config):
    """Set up a timer."""
    component = EntityComponent(_LOGGER, DOMAIN, hass)

    entities = []

    for object_id, cfg in config[DOMAIN].items():
        if not cfg:
            cfg = {}

        name = cfg.get(CONF_NAME)
        icon = cfg.get(CONF_ICON)
        restore = cfg.get(CONF_RESTORE)
        duration = cfg.get(CONF_DURATION)
        initial = cfg.get(CONF_INITIAL)
        min = cfg.get(CONF_MIN)
        max = cfg.get(CONF_MAX)
        debounce = cfg.get(CONF_DEBOUNCE)

        entities.append(TimeCounter(hass, object_id, name, icon, restore, duration, initial, min, max, debounce))

    if not entities:
        return False

    component.async_register_entity_service(
        SERVICE_UPCOUNT, SERVICE_SCHEMA,
        'async_upcount')
    component.async_register_entity_service(
        SERVICE_DOWNCOUNT, SERVICE_SCHEMA,
        'async_downcount')
    component.async_register_entity_service(
        SERVICE_STOP, SERVICE_SCHEMA,
        'async_stop')
    component.async_register_entity_service(
        SERVICE_SET, SERVICE_SCHEMA_SET,
        'async_set')

    await component.async_add_entities(entities)
    return True


class TimeCounter(RestoreEntity):
    """Representation of a timer."""

################ Constructor ################

    def __init__(self, hass, object_id, name, icon, restore, duration, initial, min, max, debounce):
        """Initialize a timer."""
        self._hass = hass

        self.entity_id = ENTITY_ID_FORMAT.format(object_id)
        self._name = name
        self._icon = icon
        self._restore = restore
        self._duration = duration
        self._min = min
        self._max = max

        self._state = self._initial = initial

        self._debounce = debounce
        self._is_locked = False

        self._debounce_listener = None # fired when debunce ends
        self._debounce_end = None # endtime of debounce

        self._mode = None # 1 for upcount or -1 for downcount
        self._target = None # max/min for upcount/downcount, target if set

        self._boundary_listener = None # fired when min or max reach
        self._boundary_end = None # endtime of reaching min or max

################ Hass methods ################

    @property
    def should_poll(self):
        """If entity should be polled."""
        return False

    @property
    def name(self):
        """Return name of the timer."""
        return self._name

    @property
    def icon(self):
        """Return the icon to be used for this entity."""
        return self._icon

    @property
    def state(self):
        """Return the current value of the timer."""
        return self._state

    @property
    def state_attributes(self):
        """Return the state attributes."""
        return {
            ATTR_DURATION: self._duration,
            ATTR_INITIAL: self._initial,
            ATTR_MIN: self._min,
            ATTR_MAX: self._max,
            ATTR_DEBOUNCE: self._debounce
        }

    async def async_added_to_hass(self):
        """Call when entity about to be added to Home Assistant."""
        await super().async_added_to_hass()
        # __init__ will set self._state to self._initial, only override
        # if needed.
        if self._restore:
            state = await self.async_get_last_state()
            if state is not None:
                self._state = float(state.state)

################ Component methods ################

    async def async_upcount(self):
        """Start upcounting."""
        _LOGGER.info("Start upcount...")
        await self.async_set(self._max)

    async def async_downcount(self):
        """Start downcounting."""
        _LOGGER.info("Start downcount...")
        await self.async_set(self._min)

    async def async_set(self, state):
        """Set given state value by running time counter for calculated amount of time."""
        if self._is_locked:
            _LOGGER.info("Timer is locked by debounce.")
            return

        self._debounce_end = dt_util.utcnow() + timedelta(seconds=(self._debounce / 1000))
        self._debounce_listener = async_track_point_in_utc_time(self._hass,
                                                       self.async_unlock_debounce,
                                                       self._debounce_end)
        self._is_locked = True

        _LOGGER.info("Setting value:")

        if state > self._max:
            state = self._max
        elif state < self._min:
            state = self._min

        _LOGGER.info(state)

        # in case other time counter is running
        await self.async_stop()

        self._target = state

        if self._state < state:
            # We will upcount.
            self._mode = MODE_UPCOUNTING
            upcount_value = state - self._state
            count_time = self._duration * upcount_value / self._max
        else:
            # We will downcount.
            self._mode = MODE_DOWNCOUNTING
            downcount_value = self._state - state
            count_time = self._duration * downcount_value / self._max

        self._boundary_end = dt_util.utcnow() + timedelta(seconds=count_time) # timedelta.seconds can be float! using it!
        _LOGGER.info("Shall end at:")
        _LOGGER.info(self._boundary_end)

        self._hass.bus.async_fire(EVENT_TIME_COUNTER_STARTED,
                                  {"entity_id": self.entity_id})

        self._boundary_listener = async_track_point_in_utc_time(self._hass,
                                                       self.async_finish,
                                                       self._boundary_end)

    async def async_stop(self):
        """Stop counter."""
        _LOGGER.info("Timer stop.")

        if self._boundary_listener:
            self._boundary_listener()
            self._boundary_listener = None

        when = dt_util.utcnow()
        await self.async_update_state(when)

################ Listener actions ################

    async def async_finish(self, time):
        """Timer stopped by reaching boundary or manual trigger."""
        _LOGGER.info("Timer finished.")

        self._boundary_listener = None

        await self.async_update_state(time)

    async def async_update_state(self, time):
        """Update the state."""
        _LOGGER.info("Updating state:")

        if self._boundary_end is None or self._mode is None or self._target is None:
            _LOGGER.info("No previously running time counter.")
            return

        time_diff = self._boundary_end - time
        time_to = time_diff.total_seconds() # timedelta.seconds can be float! using it!

        if time_to < 0:
            time_to = 0

        if self._mode > 0:
            # Upcounting.
            self._state = self._target - (time_to * self._max / self._duration)
        else:
            # Downcounting.
            self._state = self._target + (time_to * self._max / self._duration)

        _LOGGER.info(self._state)

        self._target = None
        self._mode = None
        self._boundary_end = None

        self._hass.bus.async_fire(EVENT_TIME_COUNTER_STOPPED,
                                  {"entity_id": self.entity_id, "state": self._state})

        await self.async_update_ha_state()

    async def async_unlock_debounce(self, time):
        """Unlocking debounce."""
        _LOGGER.info("Debounce unlocked.")
        self._is_locked = False
        self._debounce_listener = None

Any feedback appreciated!

3 Likes

Something broke after I downloaded the latest version of time_counter (or perhaps it was the to Hassio 0.92 update which broke some things and had to restore 0.91.4)

Covers work but the time_counter is not. Therefore the slider does not update and neither do the cover controls. Using the slider in the ā€˜more infoā€™ box a couple of times usually updates the position.

Any suggestions would be most welcome.

Found it. Forgot to add initial_state:true. Automations were going OFF after Hassio restart. What an idiot I am!

BTW is there any way that we can see what the shutter is doing?

This could be the position updating in real time or a state showing ā€˜openingā€™ or ā€˜closingā€™.

Anything that shows that one of the relays is on.

Thanks

Hi, Iā€™m new to the forum and home assistant, I took a guide for the time but I donā€™t know how it fits in hassio.
can you help me?
Thank you
alex

Iā€™m sorry, I donā€™t understand what you are askingā€¦

sorry for the translation, the question is how do you integrate the time_counter component into hassio?

here is the basics of what is needed:

create a directory in the custom_components directory in your config folder called ā€œtime_counterā€.

in that directory that you just created, make a new file called "__init__.py". (you need two underscores before and after ā€œinitā€).

in that file you need to copy the contents of the above code block and paste them in. Save the file and restart HA.

hopefully everything will work because the way that HA handles custom components has changed in the latest releases. You may need a services.yaml but Iā€™m not sure how to write one that will work. Or maybe you wonā€™t need one but you will get warnings in the log that will say that it canā€™t find one. but it might still work without one.

once you have the file above then try to enter a test configuration into your configuration.yaml using the example above and see if it works.

thank you very much for the support.
I will try immediately

the component works. thanks for the help, now I have another problem. I have just configured everything with time_counter but despite having put 20 seconds of time the relay always stops at 10 seconds.
It also doesnā€™t fit the position and I donā€™t know how to calibrate.
here is my configuration.
can you help me?

I had forgotten the pulsetime command active on the sonoff, now the time is respected. But the problem remains that the slide does not matchā€¦For example if I put the cursor on 25% he moves up to 36% or 38% etc.
How can I solve this problem?

@Batteralex unfortunately time_counter is based on built-in HomeAssistant timer that is far away from being precise. Iā€™m planning to rewrite it to Pythonā€™s timer to keep values more precise.

@PierreScerri I once had version with live update, but it caused a lot of UnknownListener errors because of async running. Are there any cover component parameters that can mark state as opening or closing?

Thanks for the reply. I donā€™t think that there are component cover parameters the can show opening or closing.
I can understand the problem with live position update.

Is there a way to show that the cover is moving by changing the colour of the appropriate arrow or the window icon? That would also work for me. I only need to know whether the cover is moving or stationary.

Hi
Iā€™m trying to convert your automations to node-red.

I have been partial successful. Shutters operate fine except that when a shutter stops Iā€™m getting a power2 off or power1 off on all the shutters.

Not sure whether itā€™s youā€™re Time_counter or my horrible Node-red setup.

I have included part of the flow to show the concept.

Thank you for any suggestion.

[{"id":"31e7746d.be376c","type":"switch","z":"9f72abab.1c47c8","name":"Stop or Go","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"ON","vt":"str"},{"t":"eq","v":"OFF","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":790,"y":180,"wires":[["520816b2.58a2b8"],["4f8630ec.14ab68"]]},{"id":"520816b2.58a2b8","type":"switch","z":"9f72abab.1c47c8","name":"Open of Close","property":"topic","propertyType":"msg","rules":[{"t":"cont","v":"POWER1","vt":"str"},{"t":"cont","v":"POWER2","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":980,"y":160,"wires":[["54c7b486.e7f9f4"],["f206e6b1.a45be8"]]},{"id":"4d1979a8.2ddc48","type":"switch","z":"9f72abab.1c47c8","name":"Stop or Go","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"ON","vt":"str"},{"t":"eq","v":"OFF","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":790,"y":300,"wires":[["53dbe954.89cfd8"],["b94acdc8.6d67d8"]]},{"id":"53dbe954.89cfd8","type":"switch","z":"9f72abab.1c47c8","name":"Open of Close","property":"topic","propertyType":"msg","rules":[{"t":"cont","v":"POWER1","vt":"str"},{"t":"cont","v":"POWER2","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":980,"y":280,"wires":[["939e7942.1b3d28"],["934c0202.1b6f18"]]},{"id":"db18c5bd.0798e","type":"switch","z":"9f72abab.1c47c8","name":"Stop or Go","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"ON","vt":"str"},{"t":"eq","v":"OFF","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":790,"y":420,"wires":[["a998f414.23e74"],["73e0f5ec.ad4774"]]},{"id":"a998f414.23e74","type":"switch","z":"9f72abab.1c47c8","name":"Open of Close","property":"topic","propertyType":"msg","rules":[{"t":"cont","v":"POWER1","vt":"str"},{"t":"cont","v":"POWER2","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":980,"y":400,"wires":[["5a6a1301.8d940c"],["8bf1d7f8.a1ae98"]]},{"id":"f206e6b1.a45be8","type":"api-call-service","z":"9f72abab.1c47c8","name":"Closing","server":"30022eef.0e4222","service_domain":"time_counter","service":"downcount","data":"{\"entity_id\":\"time_counter.sitting_room_front_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1160,"y":160,"wires":[["b22017d.6886f68"]]},{"id":"4f8630ec.14ab68","type":"api-call-service","z":"9f72abab.1c47c8","name":"Stopping","server":"30022eef.0e4222","service_domain":"time_counter","service":"stop","data":"{\"entity_id\":\"time_counter.sitting_room_front_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":960,"y":200,"wires":[["b22017d.6886f68"]]},{"id":"54c7b486.e7f9f4","type":"api-call-service","z":"9f72abab.1c47c8","name":"Opening","server":"30022eef.0e4222","service_domain":"time_counter","service":"upcount","data":"{\"entity_id\":\"time_counter.sitting_room_front_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1160,"y":120,"wires":[["b22017d.6886f68"]]},{"id":"8bf1d7f8.a1ae98","type":"api-call-service","z":"9f72abab.1c47c8","name":"Closing","server":"30022eef.0e4222","service_domain":"time_counter","service":"downcount","data":"{\"entity_id\":\"time_counter.sitting_room_back_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1160,"y":400,"wires":[["6c82e2ad.be8c34"]]},{"id":"73e0f5ec.ad4774","type":"api-call-service","z":"9f72abab.1c47c8","name":"Stopping","server":"30022eef.0e4222","service_domain":"time_counter","service":"stop","data":"{\"entity_id\":\"time_counter.sitting_room_back_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":960,"y":440,"wires":[["6c82e2ad.be8c34"]]},{"id":"5a6a1301.8d940c","type":"api-call-service","z":"9f72abab.1c47c8","name":"Opening","server":"30022eef.0e4222","service_domain":"time_counter","service":"upcount","data":"{\"entity_id\":\"time_counter.sitting_room_back_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1160,"y":360,"wires":[["6c82e2ad.be8c34"]]},{"id":"934c0202.1b6f18","type":"api-call-service","z":"9f72abab.1c47c8","name":"Closing","server":"30022eef.0e4222","service_domain":"time_counter","service":"downcount","data":"{\"entity_id\":\"time_counter.sitting_room_door_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1160,"y":280,"wires":[["44dd7d46.11f7dc"]]},{"id":"b94acdc8.6d67d8","type":"api-call-service","z":"9f72abab.1c47c8","name":"Stopping","server":"30022eef.0e4222","service_domain":"time_counter","service":"stop","data":"{\"entity_id\":\"time_counter.sitting_room_door_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":960,"y":320,"wires":[["44dd7d46.11f7dc"]]},{"id":"939e7942.1b3d28","type":"api-call-service","z":"9f72abab.1c47c8","name":"Opening","server":"30022eef.0e4222","service_domain":"time_counter","service":"upcount","data":"{\"entity_id\":\"time_counter.sitting_room_door_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1160,"y":240,"wires":[["44dd7d46.11f7dc"]]},{"id":"5d8dceac.d6fb6","type":"server-events","z":"9f72abab.1c47c8","name":"Stopped","server":"30022eef.0e4222","event_type":"time_counter.stopped","x":1100,"y":200,"wires":[["27572cd6.7a4e74"]]},{"id":"27572cd6.7a4e74","type":"api-call-service","z":"9f72abab.1c47c8","name":"Stop Cover","server":"30022eef.0e4222","service_domain":"cover","service":"stop_cover","data":"{\"entity_id\":\"cover.sitting_room_front_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1250,"y":200,"wires":[[]]},{"id":"4e8331d3.14be8","type":"server-events","z":"9f72abab.1c47c8","name":"Stopped","server":"30022eef.0e4222","event_type":"time_counter.stopped","x":1100,"y":320,"wires":[["9af872f6.96b9b8"]]},{"id":"9af872f6.96b9b8","type":"api-call-service","z":"9f72abab.1c47c8","name":"Stop Cover","server":"30022eef.0e4222","service_domain":"cover","service":"stop_cover","data":"{\"entity_id\":\"cover.sitting_room_door_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1250,"y":320,"wires":[[]]},{"id":"311b2db1.7820ba","type":"server-events","z":"9f72abab.1c47c8","name":"Stopped","server":"30022eef.0e4222","event_type":"time_counter.stopped","x":1100,"y":440,"wires":[["22a7e65b.61ae12"]]},{"id":"22a7e65b.61ae12","type":"api-call-service","z":"9f72abab.1c47c8","name":"Stop Cover","server":"30022eef.0e4222","service_domain":"cover","service":"stop_cover","data":"{\"entity_id\":\"cover.sitting_room_back_shutter\"}","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1250,"y":440,"wires":[[]]},{"id":"a39b5ab3.657768","type":"comment","z":"9f72abab.1c47c8","name":"Sitting Room Front Shutter","info":"","x":830,"y":120,"wires":[]},{"id":"b48b1996.65373","type":"comment","z":"9f72abab.1c47c8","name":"Sitting Room Door Shutter","info":"","x":830,"y":240,"wires":[]},{"id":"94eb878.b3a4c78","type":"comment","z":"9f72abab.1c47c8","name":"Sitting Room Back Shutter","info":"","x":830,"y":360,"wires":[]},{"id":"b22017d.6886f68","type":"debug","z":"9f72abab.1c47c8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1430,"y":120,"wires":[]},{"id":"44dd7d46.11f7dc","type":"debug","z":"9f72abab.1c47c8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1430,"y":240,"wires":[]},{"id":"6c82e2ad.be8c34","type":"debug","z":"9f72abab.1c47c8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1430,"y":360,"wires":[]},{"id":"2faa973c.790a1","type":"switch","z":"9f72abab.1c47c8","name":"Which Shutter?","property":"topic","propertyType":"msg","rules":[{"t":"cont","v":"sonoff03","vt":"str"},{"t":"cont","v":"sonoff12","vt":"str"},{"t":"cont","v":"sonoff11","vt":"str"},{"t":"cont","v":"sonoff04","vt":"str"},{"t":"cont","v":"sonoff05","vt":"str"},{"t":"cont","v":"sonoff06","vt":"str"},{"t":"cont","v":"sonoff07","vt":"str"},{"t":"cont","v":"sonoff08","vt":"str"},{"t":"cont","v":"sonoff09","vt":"str"},{"t":"cont","v":"sonoff10","vt":"str"},{"t":"cont","v":"sonoff14","vt":"str"}],"checkall":"true","repair":false,"outputs":11,"x":420,"y":260,"wires":[[],[],[],["31e7746d.be376c"],["4d1979a8.2ddc48"],["db18c5bd.0798e"],[],[],[],[],[]]},{"id":"1631ad59.47a7a3","type":"switch","z":"9f72abab.1c47c8","name":"POWER ?","property":"topic","propertyType":"msg","rules":[{"t":"cont","v":"POWER1","vt":"str"},{"t":"cont","v":"POWER2","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":240,"y":260,"wires":[["2faa973c.790a1"],["2faa973c.790a1"]]},{"id":"44af20ca.914ff8","type":"mqtt in","z":"9f72abab.1c47c8","name":"Action","topic":"stat/+/+","qos":"2","datatype":"auto","broker":"c28a2dad.22663","x":90,"y":260,"wires":[["1631ad59.47a7a3"]]},{"id":"30022eef.0e4222","type":"server","z":"","name":"Home Assistant","legacy":false,"hassio":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true},{"id":"c28a2dad.22663","type":"mqtt-broker","z":"","name":"","broker":"192.168.1.200","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","willTopic":"","willQos":"0","willRetain":"false","willPayload":""}]

hey,
Iā€™ve installed your code.
THANKS!

can you give me an example how do I use it
to control my smart curtain (10 seconds from fully closed to fully open)

thanks in advanced

Hi,
can you help me to use the component?
I have a shelly 2.5 setting as a cover : cover.shelly_shsw_25_1103f6 and time_counter.tapparella_sala

in Configuration.yaml:
time_counter:
tapparella_sala:
name: ā€œTapparella sala position counterā€
duration: 16
min: 0
max: 100
debounce: 1000

in automation.yaml:

  • id: ā€˜1586345077268ā€™
    alias: Tapparella Sala closing
    description: ā€˜ā€™
    trigger:

    • entity_id: cover.shelly_shsw_25_1103f6
      platform: state
      to: closing
      condition:
      action:
    • data: {}
      service: time_counter.downcount
      entity_id: time_counter.tapparella_sala
  • id: ā€˜1586345113721ā€™
    alias: Tapparella Sala opening
    description: ā€˜ā€™
    trigger:

    • entity_id: cover.shelly_shsw_25_1103f6
      platform: state
      to: opening
      condition:
      action:
    • data: {}
      service: time_counter.downcount
      entity_id: time_counter.tapparella_sala
  • id: ā€˜1586345181434ā€™
    alias: Tapparella Sala Stop
    description: ā€˜ā€™
    trigger:

    • entity_id: cover.shelly_shsw_25_1103f6
      platform: state
      to: unknow
      condition:
      action:
    • data: {}
      service: time_counter.stop
      entity_id: time_counter.tapparella_sala

by opening or closing the cover, the automation starts but the value does not change.

Where am I wrong?
Can you help me please.

Thanks in advance

@davanda please use ``` [enter] code here [enter] ``` format while pasting code. :slight_smile:

Iā€™m not sure how Shelly integrates with HA, but if it creates a switch entities per relay, you can use it like this:

This assumes your covers are 2-gang controlled: press one button to up and another to down.

# File: automations.yml

- alias: 'Internal: Living room cover - started'
  trigger:
    - platform: state
      entity_id: 'switch.cover_up'
      to: 'ON'
    - platform: state
      entity_id: 'switch.cover_down'
      to: 'ON'
  action:
    - service_template: >
        {% if trigger.entity_id == 'switch.cover_down' %}
          time_counter.downcount
        {% elif trigger.entity_id == 'switch.cover_up' %}
          time_counter.upcount
        {% endif %}
      entity_id: time_counter.cover_living_room

- alias: 'Internal: Living room cover - stopped'
  trigger:
    - platform: state
      entity_id: 'switch.cover_up'
      to: 'OFF'
    - platform: state
      entity_id: 'switch.cover_down'
      to: 'OFF'
  action:
    - service: time_counter.stop
      entity_id: time_counter.cover_living_room

- alias: Living room cover - time counter stopped
  trigger:
    platform: event
    event_type: time_counter.stopped
    event_data:
      entity_id: time_counter.cover_living_room
  action:
    - service: cover.stop_cover
      entity_id: cover.roleta_salon

For MQTT cover itā€™s similar:

# File: automations.yml

- alias: 'Internal: Living room cover - started'
  trigger:
    - platform: mqtt
      topic: 'stat/sonoff-t1/roleta-salon/+'
      payload: 'ON'
  action:
    - service_template: >
        {% if trigger.topic.split('/')[-1] == 'POWER1' %}
          time_counter.downcount
        {% elif trigger.topic.split('/')[-1] == 'POWER2' %}
          time_counter.upcount
        {% endif %}
      entity_id: time_counter.cover_living_room

- alias: 'Internal: Living room cover - stopped'
  trigger:
    - platform: mqtt
      topic: 'stat/sonoff-t1/roleta-salon/+'
      payload: 'OFF'
  action:
    - service: time_counter.stop
      entity_id: time_counter.cover_living_room

- alias: Living room cover - time counter stopped
  trigger:
    platform: event
    event_type: time_counter.stopped
    event_data:
      entity_id: time_counter.cover_living_room
  action:
    - service: cover.stop_cover
      entity_id: cover.roleta_salon

1 Like

Sorry for the format.
Shelly in cover mode creates a single component that has three states:
opening
closing
unknow (stop)

I try immediately

I made this change for the cover entity.

Everything works.

Only a question:
Can I set the position value to moving cover directly to that position?

Thanks


- id: '1586345077268'    
  alias: 'Internal: Tapparella Sala- started'
  trigger:
    - platform: state
      entity_id: cover.shelly_shsw_25
      to: 'opening'
    - platform: state
      entity_id: cover.shelly_shsw_25
      to: 'closing'
  action:
    - service_template: >
        {% if is_state('cover.shelly_shsw_25', 'opening') %}
          time_counter.downcount
        {% elif is_state('cover.shelly_shsw_25', 'closing') %}
          time_counter.upcount
        {% endif %}
      entity_id: time_counter.tapparella_sala
    
- id: '1586345113721'
  alias: 'Internal: Tapparella Sala - stopped'
  trigger:
    - platform: state
      entity_id: cover.shelly_shsw_25
      to: 'unknown'
  action:
    - service: time_counter.stop
      entity_id: time_counter.tapparella_sala
      
- id: '1586345181434'
  alias: Tapparella Sala - time counter stopped
  trigger:
    platform: event
    event_type: time_counter.stopped
    event_data:
      entity_id: time_counter.tapparella_sala
  action:
    - service: cover.stop_cover
      entity_id: cover.shelly_shsw_25

Youā€™d must introduce a new input with range probably and compare cover position with time_counter value.

I was, on the other hand, making it work for me within configuration.yml:

cover:
  - platform: template
    covers:
      living_room:
        # ...
        position_template: '{{ (states.time_counter.cover_living_room.state|int) }}'
        set_cover_position:
          - service_template: >
              {% if position > states.cover.roleta_salon.attributes.current_position|int %}
                cover.open_cover
              {% elif position < states.cover.roleta_salon.attributes.current_position|int %}
                cover.close_cover
              {% endif %}
            entity_id: cover.roleta_salon
          - service: time_counter.set
            entity_id: time_counter.cover_living_room
            data_template:
              state: '{{ position }}'

        # ...

Thanks.
Iā€™m trying it