Add brightness_step to light.turn_on (#31452)

* Clean up light turn on service

* Add brightness_step to turn_on schema

* Fix import

* Fix imports 2

* Fix RFLink test
pull/31480/head
Paulus Schoutsen 2020-02-04 16:13:29 -08:00 committed by GitHub
parent e970177eeb
commit c85a7934ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 115 additions and 73 deletions

View File

@ -1,5 +1,4 @@
"""Provides functionality to interact with lights.""" """Provides functionality to interact with lights."""
import asyncio
import csv import csv
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -8,15 +7,12 @@ from typing import Dict, Optional, Tuple
import voluptuous as vol import voluptuous as vol
from homeassistant.auth.permissions.const import POLICY_CONTROL
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TOGGLE, SERVICE_TOGGLE,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_ON, STATE_ON,
) )
from homeassistant.exceptions import Unauthorized, UnknownUser
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
@ -61,6 +57,8 @@ ATTR_WHITE_VALUE = "white_value"
# Brightness of the light, 0..255 or percentage # Brightness of the light, 0..255 or percentage
ATTR_BRIGHTNESS = "brightness" ATTR_BRIGHTNESS = "brightness"
ATTR_BRIGHTNESS_PCT = "brightness_pct" ATTR_BRIGHTNESS_PCT = "brightness_pct"
ATTR_BRIGHTNESS_STEP = "brightness_step"
ATTR_BRIGHTNESS_STEP_PCT = "brightness_step_pct"
# String representing a profile (built-in ones or external defined). # String representing a profile (built-in ones or external defined).
ATTR_PROFILE = "profile" ATTR_PROFILE = "profile"
@ -87,12 +85,16 @@ LIGHT_PROFILES_FILE = "light_profiles.csv"
VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553)) VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
VALID_BRIGHTNESS_STEP = vol.All(vol.Coerce(int), vol.Clamp(min=-255, max=255))
VALID_BRIGHTNESS_STEP_PCT = vol.All(vol.Coerce(float), vol.Clamp(min=-100, max=100))
LIGHT_TURN_ON_SCHEMA = { LIGHT_TURN_ON_SCHEMA = {
vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string,
ATTR_TRANSITION: VALID_TRANSITION, ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: VALID_BRIGHTNESS, vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP,
vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT,
vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All(
vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple)
@ -169,7 +171,7 @@ def preprocess_turn_off(params):
"""Process data for turning light off if brightness is 0.""" """Process data for turning light off if brightness is 0."""
if ATTR_BRIGHTNESS in params and params[ATTR_BRIGHTNESS] == 0: if ATTR_BRIGHTNESS in params and params[ATTR_BRIGHTNESS] == 0:
# Zero brightness: Light will be turned off # Zero brightness: Light will be turned off
params = {k: v for k, v in params.items() if k in [ATTR_TRANSITION, ATTR_FLASH]} params = {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)}
return (True, params) # Light should be turned off return (True, params) # Light should be turned off
return (False, None) # Light should be turned on return (False, None) # Light should be turned on
@ -187,70 +189,65 @@ async def async_setup(hass, config):
if not profiles_valid: if not profiles_valid:
return False return False
async def async_handle_light_on_service(service): def preprocess_data(data):
"""Handle a turn light on service call.""" """Preprocess the service data."""
# Get the validated data base = {}
params = service.data.copy()
# Convert the entity ids to valid light ids for entity_field in cv.ENTITY_SERVICE_FIELDS:
target_lights = await component.async_extract_from_service(service) if entity_field in data:
params.pop(ATTR_ENTITY_ID, None) base[entity_field] = data.pop(entity_field)
if service.context.user_id: preprocess_turn_on_alternatives(data)
user = await hass.auth.async_get_user(service.context.user_id) turn_lights_off, off_params = preprocess_turn_off(data)
if user is None:
raise UnknownUser(context=service.context)
entity_perms = user.permissions.check_entity base["params"] = data
base["turn_lights_off"] = turn_lights_off
base["off_params"] = off_params
for light in target_lights: return base
if not entity_perms(light, POLICY_CONTROL):
raise Unauthorized(
context=service.context,
entity_id=light,
permission=POLICY_CONTROL,
)
preprocess_turn_on_alternatives(params) async def async_handle_light_on_service(light, call):
turn_lights_off, off_params = preprocess_turn_off(params) """Handle turning a light on.
poll_lights = [] If brightness is set to 0, this service will turn the light off.
change_tasks = [] """
for light in target_lights: params = call.data["params"]
light.async_set_context(service.context) turn_light_off = call.data["turn_lights_off"]
off_params = call.data["off_params"]
if not params:
default_profile = Profiles.get_default(light.entity_id)
if default_profile is not None:
params = {ATTR_PROFILE: default_profile}
preprocess_turn_on_alternatives(params)
turn_light_off, off_params = preprocess_turn_off(params)
elif ATTR_BRIGHTNESS_STEP in params or ATTR_BRIGHTNESS_STEP_PCT in params:
brightness = light.brightness if light.is_on else 0
params = params.copy()
if ATTR_BRIGHTNESS_STEP in params:
brightness += params.pop(ATTR_BRIGHTNESS_STEP)
pars = params
off_pars = off_params
turn_light_off = turn_lights_off
if not pars:
pars = params.copy()
pars[ATTR_PROFILE] = Profiles.get_default(light.entity_id)
preprocess_turn_on_alternatives(pars)
turn_light_off, off_pars = preprocess_turn_off(pars)
if turn_light_off:
task = light.async_request_call(light.async_turn_off(**off_pars))
else: else:
task = light.async_request_call(light.async_turn_on(**pars)) brightness += int(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255)
change_tasks.append(task) params[ATTR_BRIGHTNESS] = max(0, min(255, brightness))
turn_light_off, off_params = preprocess_turn_off(params)
if light.should_poll: if turn_light_off:
poll_lights.append(light) await light.async_turn_off(**off_params)
else:
if change_tasks: await light.async_turn_on(**params)
await asyncio.wait(change_tasks)
if poll_lights:
await asyncio.wait(
[light.async_update_ha_state(True) for light in poll_lights]
)
# Listen for light on and light off service calls. # Listen for light on and light off service calls.
hass.services.async_register(
DOMAIN, component.async_register_entity_service(
SERVICE_TURN_ON, SERVICE_TURN_ON,
vol.All(cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), preprocess_data),
async_handle_light_on_service, async_handle_light_on_service,
schema=cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA),
) )
component.async_register_entity_service( component.async_register_entity_service(

View File

@ -1,6 +1,7 @@
"""Intents for the light integration.""" """Intents for the light integration."""
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -8,7 +9,6 @@ import homeassistant.util.color as color_util
from . import ( from . import (
ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_PCT,
ATTR_ENTITY_ID,
ATTR_RGB_COLOR, ATTR_RGB_COLOR,
DOMAIN, DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,

View File

@ -36,6 +36,12 @@ turn_on:
brightness_pct: brightness_pct:
description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light.
example: 47 example: 47
brightness_step:
description: Change brightness by an amount. Should be between -255..255.
example: -25.5
brightness_step_pct:
description: Change brightness by a percentage. Should be between -100..100.
example: -10
profile: profile:
description: Name of a light profile to use. description: Name of a light profile to use.
example: relax example: relax

View File

@ -19,7 +19,6 @@ import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP, ATTR_COLOR_TEMP,
ATTR_ENTITY_ID,
ATTR_HS_COLOR, ATTR_HS_COLOR,
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS, SUPPORT_BRIGHTNESS,
@ -27,7 +26,7 @@ from homeassistant.components.light import (
SUPPORT_COLOR_TEMP, SUPPORT_COLOR_TEMP,
Light, Light,
) )
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import color, dt from homeassistant.util import color, dt

View File

@ -462,3 +462,37 @@ async def test_light_turn_on_auth(hass, hass_admin_user):
True, True,
core.Context(user_id=hass_admin_user.id), core.Context(user_id=hass_admin_user.id),
) )
async def test_light_brightness_step(hass):
"""Test that light context works."""
platform = getattr(hass.components, "test.light")
platform.init()
entity = platform.ENTITIES[0]
entity.supported_features = light.SUPPORT_BRIGHTNESS
entity.brightness = 100
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
state = hass.states.get(entity.entity_id)
assert state is not None
assert state.attributes["brightness"] == 100
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": entity.entity_id, "brightness_step": -10},
True,
)
_, data = entity.last_call("turn_on")
assert data["brightness"] == 90, data
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": entity.entity_id, "brightness_step_pct": 10},
True,
)
_, data = entity.last_call("turn_on")
assert data["brightness"] == 125, data

View File

@ -298,18 +298,16 @@ async def test_signal_repetitions_cancelling(hass, monkeypatch):
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + ".test"} DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + ".test"}
) )
hass.async_create_task( await hass.services.async_call(
hass.services.async_call( DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DOMAIN + ".test"}, blocking=True
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DOMAIN + ".test"}
)
) )
await hass.async_block_till_done() assert [call[0][1] for call in protocol.send_command_ack.call_args_list] == [
"off",
assert protocol.send_command_ack.call_args_list[0][0][1] == "off" "on",
assert protocol.send_command_ack.call_args_list[1][0][1] == "on" "on",
assert protocol.send_command_ack.call_args_list[2][0][1] == "on" "on",
assert protocol.send_command_ack.call_args_list[3][0][1] == "on" ]
async def test_type_toggle(hass, monkeypatch): async def test_type_toggle(hass, monkeypatch):

View File

@ -3,6 +3,7 @@ Provide a mock light platform.
Call init before using it in your tests to ensure clean test data. Call init before using it in your tests to ensure clean test data.
""" """
from homeassistant.components.light import Light
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from tests.common import MockToggleEntity from tests.common import MockToggleEntity
@ -18,9 +19,9 @@ def init(empty=False):
[] []
if empty if empty
else [ else [
MockToggleEntity("Ceiling", STATE_ON), MockLight("Ceiling", STATE_ON),
MockToggleEntity("Ceiling", STATE_OFF), MockLight("Ceiling", STATE_OFF),
MockToggleEntity(None, STATE_OFF), MockLight(None, STATE_OFF),
] ]
) )
@ -30,3 +31,10 @@ async def async_setup_platform(
): ):
"""Return mock entities.""" """Return mock entities."""
async_add_entities_callback(ENTITIES) async_add_entities_callback(ENTITIES)
class MockLight(MockToggleEntity, Light):
"""Mock light class."""
brightness = None
supported_features = 0