Throttle service calls

I’m building a simple integration that will add a single service to HA. It will allow controlling fan speed.
I have the basic template ready but I want to add some more constraints by limiting for often the service can be called.

I found a post that describes the usage of @Throttle attribute, but I’m not sure if this is the best way to go with a service.

Should I add @Throttle(MIN_TIME_BETWEEN_UPDATES) to set_fan_speed function?
EDIT: tried that, doesn’t work

Maybe there is a better way of doing this in Home Assistant? I’d like to avoid creating throttling logic by hand.

Below is my template:

import asyncio
import logging
import voluptuous as vol

from homeassistant.core import callback, Config, HomeAssistant
from datetime import timedelta
from homeassistant.util import Throttle

MIN_TIME_BETWEEN_SCANS = timedelta(seconds=2)
DOMAIN = "test"
SERVICE_SET_FAN_SPEED="test_event"
ATTR_NAME = "speed"

_LOGGER = logging.getLogger(__name__)

SET_FAN_SPEED_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_NAME): vol.All(vol.Coerce(int),vol.Range(min=0, max=100)),
    }
)
async def async_setup(hass: HomeAssistant, config: Config):

    async def set_fan_speed(call):
        value = call.data.get(ATTR_NAME)
        _LOGGER.debug('Set fan speed to %s', value)

    # Register our service with Home Assistant.
    hass.services.async_register(DOMAIN, 'set_fan_speed', set_fan_speed, SET_FAN_SPEED_SCHEMA)

    return True

Can you post the code with the @Throttle in use? I’m pretty sure it should have worked.

I’ve added Throttle attribute just above callback definition:

import asyncio
import logging
import voluptuous as vol

from homeassistant.core import callback, Config, HomeAssistant
from datetime import timedelta
from homeassistant.util import Throttle

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=2)
DOMAIN = "test"
SERVICE_SET_FAN_SPEED="test_event"
ATTR_NAME = "speed"

_LOGGER = logging.getLogger(__name__)

SET_FAN_SPEED_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_NAME): vol.All(vol.Coerce(int),vol.Range(min=0, max=100)),
    }
)
async def async_setup(hass: HomeAssistant, config: Config):

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    async def set_fan_speed(call):
        value = call.data.get(ATTR_NAME)
        _LOGGER.debug('Set fan speed to %s', value)

    # Register our service with Home Assistant.
    hass.services.async_register(DOMAIN, 'set_fan_speed', set_fan_speed, SET_FAN_SPEED_SCHEMA)

    return True

I’ve tested my service via developer tools. With that attribute, I don’t see anything in the log, but when I remove that attribute everything works just fine (except calls aren’t throttled)

Ok, I think I know why it’s not working. If the function or method that uses the Throttle decorator is a coroutine function, then it must be called in an await statement. Unfortunately, the function “returned” by the Throttle decorator is not a coroutine function, and when passed into hass.services.async_register, will, therefore, not be seen as a coroutine function, so will not be awaited. (Instead it will simply be directly called.) This leads to the following exception:

/usr/lib/python3.7/asyncio/base_events.py:1772: RuntimeWarning: coroutine 'async_setup.<locals>.set_fan_speed' was never awaited
  handle = None  # Needed to break cycles when an exception occurs.
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

Therefore the service handler (on which the Throttle decorator is used) must not be defined as a coroutine function, otherwise it won’t be handled properly by hass.services.async_register.

To fix the problem, simply remove async in the definition of set_fan_speed().

If you still want that function to be mostly implemented as async, then you’ll need to define two functions, with the following probably being the easiest way:

async def async_setup(hass: HomeAssistant, config: Config):

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    async def async_set_fan_speed(call):
        value = call.data.get(ATTR_NAME)
        _LOGGER.debug('Set fan speed to %s', value)

    async def set_fan_speed(call):
        await async_set_fan_speed(call)

    # Register our service with Home Assistant.
    hass.services.async_register(DOMAIN, 'set_fan_speed', set_fan_speed, SET_FAN_SPEED_SCHEMA)

    return True
1 Like

Phil this works great. Thank you for the detailed explanation.
I will be using RPI.GPIO and my code will be mostly sync so I stick with removing the async keyword.

BTW I’ve checked both versions. Both work :grinning:

1 Like