mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Add new number entity integration (#42735)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
6c9c280bbb
commit
f744f7c34e
@ -308,6 +308,7 @@ homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
|
||||
homeassistant/components/nuheat/* @bdraco
|
||||
homeassistant/components/nuki/* @pschmitt @pvizeli
|
||||
homeassistant/components/numato/* @clssn
|
||||
homeassistant/components/number/* @home-assistant/core @Shulyaka
|
||||
homeassistant/components/nut/* @bdraco
|
||||
homeassistant/components/nws/* @MatthewFlamm
|
||||
homeassistant/components/nzbget/* @chriscla
|
||||
|
@ -19,6 +19,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"number",
|
||||
"sensor",
|
||||
"switch",
|
||||
"vacuum",
|
||||
|
130
homeassistant/components/demo/number.py
Normal file
130
homeassistant/components/demo/number.py
Normal file
@ -0,0 +1,130 @@
|
||||
"""Demo platform that offers a fake Number entity."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.number import NumberEntity
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the demo Number entity."""
|
||||
async_add_entities(
|
||||
[
|
||||
DemoNumber(
|
||||
"volume1",
|
||||
"volume",
|
||||
42.0,
|
||||
"mdi:volume-high",
|
||||
False,
|
||||
),
|
||||
DemoNumber(
|
||||
"pwm1",
|
||||
"PWM 1",
|
||||
42.0,
|
||||
"mdi:square-wave",
|
||||
False,
|
||||
0.0,
|
||||
1.0,
|
||||
0.01,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Demo config entry."""
|
||||
await async_setup_platform(hass, {}, async_add_entities)
|
||||
|
||||
|
||||
class DemoNumber(NumberEntity):
|
||||
"""Representation of a demo Number entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id,
|
||||
name,
|
||||
state,
|
||||
icon,
|
||||
assumed,
|
||||
min_value=None,
|
||||
max_value=None,
|
||||
step=None,
|
||||
):
|
||||
"""Initialize the Demo Number entity."""
|
||||
self._unique_id = unique_id
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._state = state
|
||||
self._icon = icon
|
||||
self._assumed = assumed
|
||||
self._min_value = min_value
|
||||
self._max_value = max_value
|
||||
self._step = step
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {
|
||||
# Serial numbers are unique identifiers within a specific domain
|
||||
(DOMAIN, self.unique_id)
|
||||
},
|
||||
"name": self.name,
|
||||
}
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for a demo Number entity."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use for device if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return if the state is based on assumptions."""
|
||||
return self._assumed
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current value."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def min_value(self):
|
||||
"""Return the minimum value."""
|
||||
return self._min_value or super().min_value
|
||||
|
||||
@property
|
||||
def max_value(self):
|
||||
"""Return the maximum value."""
|
||||
return self._max_value or super().max_value
|
||||
|
||||
@property
|
||||
def step(self):
|
||||
"""Return the value step."""
|
||||
return self._step or super().step
|
||||
|
||||
async def async_set_value(self, value):
|
||||
"""Update the current value."""
|
||||
num_value = float(value)
|
||||
|
||||
if num_value < self.min_value or num_value > self.max_value:
|
||||
raise vol.Invalid(
|
||||
f"Invalid value for {self.entity_id}: {value} (range {self.min_value} - {self.max_value})"
|
||||
)
|
||||
|
||||
self._state = num_value
|
||||
self.async_write_ha_state()
|
103
homeassistant/components/number/__init__.py
Normal file
103
homeassistant/components/number/__init__.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""Component to allow numeric input for platforms."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
|
||||
from .const import (
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_STEP,
|
||||
ATTR_VALUE,
|
||||
DEFAULT_MAX_VALUE,
|
||||
DEFAULT_MIN_VALUE,
|
||||
DEFAULT_STEP,
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||
"""Set up Number entities."""
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||
)
|
||||
await component.async_setup(config)
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_VALUE,
|
||||
{vol.Required(ATTR_VALUE): vol.Coerce(float)},
|
||||
"async_set_value",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry) # type: ignore
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry) # type: ignore
|
||||
|
||||
|
||||
class NumberEntity(Entity):
|
||||
"""Representation of a Number entity."""
|
||||
|
||||
@property
|
||||
def capability_attributes(self) -> Dict[str, Any]:
|
||||
"""Return capability attributes."""
|
||||
return {
|
||||
ATTR_MIN: self.min_value,
|
||||
ATTR_MAX: self.max_value,
|
||||
ATTR_STEP: self.step,
|
||||
}
|
||||
|
||||
@property
|
||||
def min_value(self) -> float:
|
||||
"""Return the minimum value."""
|
||||
return DEFAULT_MIN_VALUE
|
||||
|
||||
@property
|
||||
def max_value(self) -> float:
|
||||
"""Return the maximum value."""
|
||||
return DEFAULT_MAX_VALUE
|
||||
|
||||
@property
|
||||
def step(self) -> float:
|
||||
"""Return the increment/decrement step."""
|
||||
step = DEFAULT_STEP
|
||||
value_range = abs(self.max_value - self.min_value)
|
||||
if value_range != 0:
|
||||
while value_range <= step:
|
||||
step /= 10.0
|
||||
return step
|
||||
|
||||
def set_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_set_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self.set_value, value)
|
14
homeassistant/components/number/const.py
Normal file
14
homeassistant/components/number/const.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""Provides the constants needed for the component."""
|
||||
|
||||
ATTR_VALUE = "value"
|
||||
ATTR_MIN = "min"
|
||||
ATTR_MAX = "max"
|
||||
ATTR_STEP = "step"
|
||||
|
||||
DEFAULT_MIN_VALUE = 0.0
|
||||
DEFAULT_MAX_VALUE = 100.0
|
||||
DEFAULT_STEP = 1.0
|
||||
|
||||
DOMAIN = "number"
|
||||
|
||||
SERVICE_SET_VALUE = "set_value"
|
7
homeassistant/components/number/manifest.json
Normal file
7
homeassistant/components/number/manifest.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"domain": "number",
|
||||
"name": "Number",
|
||||
"documentation": "https://www.home-assistant.io/integrations/number",
|
||||
"codeowners": ["@home-assistant/core", "@Shulyaka"],
|
||||
"quality_scale": "internal"
|
||||
}
|
11
homeassistant/components/number/services.yaml
Normal file
11
homeassistant/components/number/services.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
# Describes the format for available Number entity services
|
||||
|
||||
set_value:
|
||||
description: Set the value of a Number entity.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity ID of the Number to set the new value.
|
||||
example: number.volume
|
||||
value:
|
||||
description: The target value the entity should be set to.
|
||||
example: 42
|
@ -40,7 +40,8 @@ warn_incomplete_stub = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_configs = true
|
||||
|
||||
[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*]
|
||||
|
||||
[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*]
|
||||
strict = true
|
||||
ignore_errors = false
|
||||
warn_unreachable = true
|
||||
|
97
tests/components/demo/test_number.py
Normal file
97
tests/components/demo/test_number.py
Normal file
@ -0,0 +1,97 @@
|
||||
"""The tests for the demo number component."""
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.number.const import (
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_STEP,
|
||||
ATTR_VALUE,
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
ENTITY_VOLUME = "number.volume"
|
||||
ENTITY_PWM = "number.pwm_1"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_demo_number(hass):
|
||||
"""Initialize setup demo Number entity."""
|
||||
assert await async_setup_component(hass, DOMAIN, {"number": {"platform": "demo"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
def test_setup_params(hass):
|
||||
"""Test the initial parameters."""
|
||||
state = hass.states.get(ENTITY_VOLUME)
|
||||
assert state.state == "42.0"
|
||||
|
||||
|
||||
def test_default_setup_params(hass):
|
||||
"""Test the setup with default parameters."""
|
||||
state = hass.states.get(ENTITY_VOLUME)
|
||||
assert state.attributes.get(ATTR_MIN) == 0.0
|
||||
assert state.attributes.get(ATTR_MAX) == 100.0
|
||||
assert state.attributes.get(ATTR_STEP) == 1.0
|
||||
|
||||
state = hass.states.get(ENTITY_PWM)
|
||||
assert state.attributes.get(ATTR_MIN) == 0.0
|
||||
assert state.attributes.get(ATTR_MAX) == 1.0
|
||||
assert state.attributes.get(ATTR_STEP) == 0.01
|
||||
|
||||
|
||||
async def test_set_value_bad_attr(hass):
|
||||
"""Test setting the value without required attribute."""
|
||||
state = hass.states.get(ENTITY_VOLUME)
|
||||
assert state.state == "42.0"
|
||||
|
||||
with pytest.raises(vol.Invalid):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_VALUE: None, ATTR_ENTITY_ID: ENTITY_VOLUME},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_VOLUME)
|
||||
assert state.state == "42.0"
|
||||
|
||||
|
||||
async def test_set_value_bad_range(hass):
|
||||
"""Test setting the value out of range."""
|
||||
state = hass.states.get(ENTITY_VOLUME)
|
||||
assert state.state == "42.0"
|
||||
|
||||
with pytest.raises(vol.Invalid):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_VALUE: 1024, ATTR_ENTITY_ID: ENTITY_VOLUME},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_VOLUME)
|
||||
assert state.state == "42.0"
|
||||
|
||||
|
||||
async def test_set_set_value(hass):
|
||||
"""Test the setting of the value."""
|
||||
state = hass.states.get(ENTITY_VOLUME)
|
||||
assert state.state == "42.0"
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_VALUE: 23, ATTR_ENTITY_ID: ENTITY_VOLUME},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_VOLUME)
|
||||
assert state.state == "23.0"
|
1
tests/components/number/__init__.py
Normal file
1
tests/components/number/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""The tests for Number integration."""
|
39
tests/components/number/test_init.py
Normal file
39
tests/components/number/test_init.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""The tests for the Number component."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant.components.number import NumberEntity
|
||||
|
||||
|
||||
class MockNumberEntity(NumberEntity):
|
||||
"""Mock NumberEntity device to use in tests."""
|
||||
|
||||
@property
|
||||
def max_value(self) -> float:
|
||||
"""Return the max value."""
|
||||
return 1.0
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current value."""
|
||||
return "0.5"
|
||||
|
||||
|
||||
async def test_step(hass):
|
||||
"""Test the step calculation."""
|
||||
number = NumberEntity()
|
||||
assert number.step == 1.0
|
||||
|
||||
number_2 = MockNumberEntity()
|
||||
assert number_2.step == 0.1
|
||||
|
||||
|
||||
async def test_sync_set_value(hass):
|
||||
"""Test if async set_value calls sync set_value."""
|
||||
number = NumberEntity()
|
||||
number.hass = hass
|
||||
|
||||
number.set_value = MagicMock()
|
||||
await number.async_set_value(42)
|
||||
|
||||
assert number.set_value.called
|
||||
assert number.set_value.call_args[0][0] == 42
|
Loading…
x
Reference in New Issue
Block a user