429 files

This commit is contained in:
Home Assistant Version Control committed 2026-08-30 02:43:31 +00:00
1 parent 472c19bceb
commit 963cce81a7
566 files changed
+50311 -47616

No files matched your search

@@ -1 +1,81 @@
"""Spook - Your homie."""
from __future__ import annotations
import math
from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
if TYPE_CHECKING:
from homeassistant.components.input_number import InputNumber
from homeassistant.core import ServiceCall
CONF_AMOUNT = "amount"
CONF_CYCLE = "cycle"
# Steps are floats, so every comparison against a step boundary needs slack.
_FLOAT_TOLERANCE = 1e-9
STEP_SERVICE_SCHEMA = {
vol.Optional(CONF_AMOUNT): vol.Coerce(float),
vol.Optional(CONF_CYCLE, default=False): cv.boolean,
}
def native_value_as_float(entity: InputNumber) -> float:
"""Return the current value of an input number as a float."""
try:
return float(entity.native_value)
except (TypeError, ValueError) as err:
msg = f"Value {entity.native_value!r} for {entity.entity_id} is not a number"
raise HomeAssistantError(msg) from err
def step_amount(entity: InputNumber, call: ServiceCall) -> float:
"""Return the requested amount, checked against the entity's step."""
step = entity.native_step
amount = call.data.get(CONF_AMOUNT, step)
remainder = amount % step
# Float modulo wraps around just short of the step, 0.3 % 0.1 is
# 0.09999999999999998, so a remainder against either end is a clean multiple.
if not (
math.isclose(remainder, 0, rel_tol=0, abs_tol=_FLOAT_TOLERANCE)
or math.isclose(remainder, step, rel_tol=0, abs_tol=_FLOAT_TOLERANCE)
):
msg = (
f"Amount {amount} not valid for {entity.entity_id}, "
f"it needs to be a multiple of {step}"
)
raise ValueError(msg)
return amount
def cycled_value(entity: InputNumber, value: float) -> float:
"""Wrap a value around the entity's range.
Home Assistant cycles a select by taking the new index modulo the number
of options. The same thing here means the positions on the step grid from
the minimum to the maximum, so overshooting one end carries on from the
other rather than landing on it. Stepping one past the maximum reaches the
minimum, and stepping five past it reaches the fifth value up.
"""
minimum = entity.native_min_value
step = entity.native_step
# Floored, because the step does not have to divide the range. Rounding
# would invent a slot above the maximum: a step of 0.6 across 0 to 1 would
# offer 1.2, and setting that raises. The tolerance keeps a quotient that
# is a whole number in spirit, like 10 steps of 0.1, from flooring down.
quotient = (entity.native_max_value - minimum) / step
slots = math.floor(quotient + _FLOAT_TOLERANCE) + 1
# Counted in whole steps, so float drift cannot push the result into the
# neighbouring slot on the way through the modulo.
offset = round((value - minimum) / step)
return minimum + (offset % slots) * step
@@ -2,14 +2,18 @@
from __future__ import annotations
import math
from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.input_number import DOMAIN, InputNumber
from ....services import AbstractSpookEntityComponentService, ReplaceExistingService
from . import (
CONF_CYCLE,
STEP_SERVICE_SCHEMA,
cycled_value,
native_value_as_float,
step_amount,
)
if TYPE_CHECKING:
from homeassistant.core import ServiceCall
@@ -20,12 +24,13 @@ class SpookService(
):
"""Input number entity service, decrease value by a single step.
It override the built-in increment service to allow for a custom amount.
It overrides the built-in decrement service to allow for a custom amount,
and to cycle around the range instead of stopping at the end of it.
"""
domain = DOMAIN
service = "decrement"
schema = {vol.Optional("amount"): vol.Coerce(float)}
schema = STEP_SERVICE_SCHEMA
async def async_handle_service(
self,
@@ -33,18 +38,16 @@ class SpookService(
call: ServiceCall,
) -> None:
"""Handle the service call."""
# pylint: disable=protected-access
amount = call.data.get("amount", entity._step) # noqa: SLF001
if not math.isclose(amount % entity._step, 0, abs_tol=1e-9): # noqa: SLF001
msg = (
f"Amount {amount} not valid for {entity.entity_id}, "
f"it needs to be a multiple of {entity._step}", # noqa: SLF001
)
raise ValueError(msg)
new_value = native_value_as_float(entity) - step_amount(entity, call)
await entity.async_set_value(
max(
entity._current_value - amount, # noqa: SLF001
entity._minimum, # noqa: SLF001
),
)
if call.data[CONF_CYCLE]:
# A negative amount moves the other way, so either end can be the
# one that gets crossed.
if not (entity.native_min_value <= new_value <= entity.native_max_value):
new_value = cycled_value(entity, new_value)
elif new_value < entity.native_min_value:
# Without cycling, only the end this action moves towards is
# clamped, which is what it did before cycling existed.
new_value = entity.native_min_value
await entity.async_set_native_value(new_value)
@@ -2,14 +2,18 @@
from __future__ import annotations
import math
from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.input_number import DOMAIN, InputNumber
from ....services import AbstractSpookEntityComponentService, ReplaceExistingService
from . import (
CONF_CYCLE,
STEP_SERVICE_SCHEMA,
cycled_value,
native_value_as_float,
step_amount,
)
if TYPE_CHECKING:
from homeassistant.core import ServiceCall
@@ -20,12 +24,13 @@ class SpookService(
):
"""Input number entity service, increase value by a single step.
It override the built-in increment service to allow for a custom amount.
It overrides the built-in increment service to allow for a custom amount,
and to cycle around the range instead of stopping at the end of it.
"""
domain = DOMAIN
service = "increment"
schema = {vol.Optional("amount"): vol.Coerce(float)}
schema = STEP_SERVICE_SCHEMA
async def async_handle_service(
self,
@@ -33,18 +38,16 @@ class SpookService(
call: ServiceCall,
) -> None:
"""Handle the service call."""
# pylint: disable=protected-access
amount = call.data.get("amount", entity._step) # noqa: SLF001
if not math.isclose(amount % entity._step, 0, abs_tol=1e-9): # noqa: SLF001
msg = (
f"Amount {amount} not valid for {entity.entity_id}, "
f"it needs to be a multiple of {entity._step}", # noqa: SLF001
)
raise ValueError(msg)
new_value = native_value_as_float(entity) + step_amount(entity, call)
await entity.async_set_value(
min(
entity._current_value + amount, # noqa: SLF001
entity._maximum, # noqa: SLF001
),
)
if call.data[CONF_CYCLE]:
# A negative amount moves the other way, so either end can be the
# one that gets crossed.
if not (entity.native_min_value <= new_value <= entity.native_max_value):
new_value = cycled_value(entity, new_value)
elif new_value > entity.native_max_value:
# Without cycling, only the end this action moves towards is
# clamped, which is what it did before cycling existed.
new_value = entity.native_max_value
await entity.async_set_native_value(new_value)
@@ -24,5 +24,4 @@ class SpookService(AbstractSpookEntityComponentService[InputNumber]):
call: ServiceCall, # noqa: ARG002
) -> None:
"""Handle the service call."""
# pylint: disable-next=protected-access
await entity.async_set_value(entity._maximum) # noqa: SLF001
await entity.async_set_native_value(entity.native_max_value)
@@ -24,5 +24,4 @@ class SpookService(AbstractSpookEntityComponentService[InputNumber]):
call: ServiceCall, # noqa: ARG002
) -> None:
"""Handle the service call."""
# pylint: disable-next=protected-access
await entity.async_set_value(entity._minimum) # noqa: SLF001
await entity.async_set_native_value(entity.native_min_value)