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
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
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.