Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new number entity integration #42735

Merged
merged 23 commits into from
Dec 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/demo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"light",
"lock",
"media_player",
"number",
"sensor",
"switch",
"vacuum",
Expand Down
130 changes: 130 additions & 0 deletions homeassistant/components/demo/number.py
Original file line number Diff line number Diff line change
@@ -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(
Shulyaka marked this conversation as resolved.
Show resolved Hide resolved
"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 changes: 103 additions & 0 deletions homeassistant/components/number/__init__.py
Original file line number Diff line number Diff line change
@@ -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
frenck marked this conversation as resolved.
Show resolved Hide resolved

@property
def step(self) -> float:
"""Return the increment/decrement step."""
step = DEFAULT_STEP
value_range = abs(self.max_value - self.min_value)
Shulyaka marked this conversation as resolved.
Show resolved Hide resolved
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."""
Shulyaka marked this conversation as resolved.
Show resolved Hide resolved
assert self.hass is not None
await self.hass.async_add_executor_job(self.set_value, value)
14 changes: 14 additions & 0 deletions homeassistant/components/number/const.py
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions homeassistant/components/number/manifest.json
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions homeassistant/components/number/services.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
97 changes: 97 additions & 0 deletions tests/components/demo/test_number.py
Original file line number Diff line number Diff line change
@@ -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"
Shulyaka marked this conversation as resolved.
Show resolved Hide resolved


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 change: 1 addition & 0 deletions tests/components/number/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""The tests for Number integration."""
Loading