Add new number entity integration (#42735)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
Shulyaka 2020-12-02 15:50:48 +03:00 committed by GitHub
parent 6c9c280bbb
commit f744f7c34e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 406 additions and 1 deletions

View File

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

View File

@ -19,6 +19,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
"light",
"lock",
"media_player",
"number",
"sensor",
"switch",
"vacuum",

View 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()

View 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)

View 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"

View 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"
}

View 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

View File

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

View 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"

View File

@ -0,0 +1 @@
"""The tests for Number integration."""

View 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