Added HomeKit fan speed based on speed_list (#19767)
Speed_list needs to be in ascending order.
This commit is contained in:
parent
208ea6eae4
commit
a94a24f6f8
5 changed files with 194 additions and 20 deletions
|
@ -113,6 +113,7 @@ CHAR_OUTLET_IN_USE = 'OutletInUse'
|
||||||
CHAR_ON = 'On'
|
CHAR_ON = 'On'
|
||||||
CHAR_POSITION_STATE = 'PositionState'
|
CHAR_POSITION_STATE = 'PositionState'
|
||||||
CHAR_ROTATION_DIRECTION = 'RotationDirection'
|
CHAR_ROTATION_DIRECTION = 'RotationDirection'
|
||||||
|
CHAR_ROTATION_SPEED = 'RotationSpeed'
|
||||||
CHAR_SATURATION = 'Saturation'
|
CHAR_SATURATION = 'Saturation'
|
||||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||||
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
||||||
|
|
|
@ -4,17 +4,20 @@ import logging
|
||||||
from pyhap.const import CATEGORY_FAN
|
from pyhap.const import CATEGORY_FAN
|
||||||
|
|
||||||
from homeassistant.components.fan import (
|
from homeassistant.components.fan import (
|
||||||
ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE,
|
ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_SPEED, ATTR_SPEED_LIST,
|
||||||
DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SUPPORT_DIRECTION,
|
DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE,
|
||||||
SUPPORT_OSCILLATE)
|
SERVICE_SET_DIRECTION, SERVICE_SET_SPEED, SUPPORT_DIRECTION,
|
||||||
|
SUPPORT_OSCILLATE, SUPPORT_SET_SPEED)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF,
|
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
SERVICE_TURN_ON, STATE_OFF, STATE_ON)
|
STATE_OFF, STATE_ON)
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory
|
from .accessories import debounce, HomeAccessory
|
||||||
from .const import (
|
from .const import (
|
||||||
CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_SWING_MODE, SERV_FANV2)
|
CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE,
|
||||||
|
SERV_FANV2)
|
||||||
|
from .util import HomeKitSpeedMapping
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -41,12 +44,18 @@ class Fan(HomeAccessory):
|
||||||
chars.append(CHAR_ROTATION_DIRECTION)
|
chars.append(CHAR_ROTATION_DIRECTION)
|
||||||
if features & SUPPORT_OSCILLATE:
|
if features & SUPPORT_OSCILLATE:
|
||||||
chars.append(CHAR_SWING_MODE)
|
chars.append(CHAR_SWING_MODE)
|
||||||
|
if features & SUPPORT_SET_SPEED:
|
||||||
|
speed_list = self.hass.states.get(self.entity_id) \
|
||||||
|
.attributes.get(ATTR_SPEED_LIST)
|
||||||
|
self.speed_mapping = HomeKitSpeedMapping(speed_list)
|
||||||
|
chars.append(CHAR_ROTATION_SPEED)
|
||||||
|
|
||||||
serv_fan = self.add_preload_service(SERV_FANV2, chars)
|
serv_fan = self.add_preload_service(SERV_FANV2, chars)
|
||||||
self.char_active = serv_fan.configure_char(
|
self.char_active = serv_fan.configure_char(
|
||||||
CHAR_ACTIVE, value=0, setter_callback=self.set_state)
|
CHAR_ACTIVE, value=0, setter_callback=self.set_state)
|
||||||
|
|
||||||
self.char_direction = None
|
self.char_direction = None
|
||||||
|
self.char_speed = None
|
||||||
self.char_swing = None
|
self.char_swing = None
|
||||||
|
|
||||||
if CHAR_ROTATION_DIRECTION in chars:
|
if CHAR_ROTATION_DIRECTION in chars:
|
||||||
|
@ -54,6 +63,10 @@ class Fan(HomeAccessory):
|
||||||
CHAR_ROTATION_DIRECTION, value=0,
|
CHAR_ROTATION_DIRECTION, value=0,
|
||||||
setter_callback=self.set_direction)
|
setter_callback=self.set_direction)
|
||||||
|
|
||||||
|
if CHAR_ROTATION_SPEED in chars:
|
||||||
|
self.char_speed = serv_fan.configure_char(
|
||||||
|
CHAR_ROTATION_SPEED, value=0, setter_callback=self.set_speed)
|
||||||
|
|
||||||
if CHAR_SWING_MODE in chars:
|
if CHAR_SWING_MODE in chars:
|
||||||
self.char_swing = serv_fan.configure_char(
|
self.char_swing = serv_fan.configure_char(
|
||||||
CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating)
|
CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating)
|
||||||
|
@ -83,6 +96,15 @@ class Fan(HomeAccessory):
|
||||||
ATTR_OSCILLATING: oscillating}
|
ATTR_OSCILLATING: oscillating}
|
||||||
self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating)
|
self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating)
|
||||||
|
|
||||||
|
@debounce
|
||||||
|
def set_speed(self, value):
|
||||||
|
"""Set state if call came from HomeKit."""
|
||||||
|
_LOGGER.debug('%s: Set speed to %d', self.entity_id, value)
|
||||||
|
speed = self.speed_mapping.speed_to_states(value)
|
||||||
|
params = {ATTR_ENTITY_ID: self.entity_id,
|
||||||
|
ATTR_SPEED: speed}
|
||||||
|
self.call_service(DOMAIN, SERVICE_SET_SPEED, params, speed)
|
||||||
|
|
||||||
def update_state(self, new_state):
|
def update_state(self, new_state):
|
||||||
"""Update fan after state change."""
|
"""Update fan after state change."""
|
||||||
# Handle State
|
# Handle State
|
||||||
|
@ -104,6 +126,14 @@ class Fan(HomeAccessory):
|
||||||
self.char_direction.set_value(hk_direction)
|
self.char_direction.set_value(hk_direction)
|
||||||
self._flag[CHAR_ROTATION_DIRECTION] = False
|
self._flag[CHAR_ROTATION_DIRECTION] = False
|
||||||
|
|
||||||
|
# Handle Speed
|
||||||
|
if self.char_speed is not None:
|
||||||
|
speed = new_state.attributes.get(ATTR_SPEED)
|
||||||
|
hk_speed_value = self.speed_mapping.speed_to_homekit(speed)
|
||||||
|
if hk_speed_value is not None and \
|
||||||
|
self.char_speed.value != hk_speed_value:
|
||||||
|
self.char_speed.set_value(hk_speed_value)
|
||||||
|
|
||||||
# Handle Oscillating
|
# Handle Oscillating
|
||||||
if self.char_swing is not None:
|
if self.char_swing is not None:
|
||||||
oscillating = new_state.attributes.get(ATTR_OSCILLATING)
|
oscillating = new_state.attributes.get(ATTR_OSCILLATING)
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
"""Collection of useful functions for the HomeKit component."""
|
"""Collection of useful functions for the HomeKit component."""
|
||||||
|
from collections import namedtuple, OrderedDict
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import media_player
|
from homeassistant.components import fan, media_player
|
||||||
from homeassistant.core import split_entity_id
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS)
|
ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS)
|
||||||
|
from homeassistant.core import split_entity_id
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util.temperature as temp_util
|
import homeassistant.util.temperature as temp_util
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF,
|
CONF_FEATURE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
|
||||||
FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_FAUCET,
|
FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, HOMEKIT_NOTIFY_ID, TYPE_FAUCET,
|
||||||
TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE)
|
TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -110,6 +112,50 @@ def validate_media_player_features(state, feature_list):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
SpeedRange = namedtuple('SpeedRange', ('start', 'target'))
|
||||||
|
SpeedRange.__doc__ += """ Maps Home Assistant speed \
|
||||||
|
values to percentage based HomeKit speeds.
|
||||||
|
start: Start of the range (inclusive).
|
||||||
|
target: Percentage to use to determine HomeKit percentages \
|
||||||
|
from HomeAssistant speed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class HomeKitSpeedMapping:
|
||||||
|
"""Supports conversion between Home Assistant and HomeKit fan speeds."""
|
||||||
|
|
||||||
|
def __init__(self, speed_list):
|
||||||
|
"""Initialize a new SpeedMapping object."""
|
||||||
|
if speed_list[0] != fan.SPEED_OFF:
|
||||||
|
_LOGGER.warning("%s does not contain the speed setting "
|
||||||
|
"%s as its first element. "
|
||||||
|
"Assuming that %s is equivalent to 'off'.",
|
||||||
|
speed_list, fan.SPEED_OFF, speed_list[0])
|
||||||
|
self.speed_ranges = OrderedDict()
|
||||||
|
list_size = len(speed_list)
|
||||||
|
for index, speed in enumerate(speed_list):
|
||||||
|
# By dividing by list_size -1 the following
|
||||||
|
# desired attributes hold true:
|
||||||
|
# * index = 0 => 0%, equal to "off"
|
||||||
|
# * index = len(speed_list) - 1 => 100 %
|
||||||
|
# * all other indices are equally distributed
|
||||||
|
target = index * 100 / (list_size - 1)
|
||||||
|
start = index * 100 / list_size
|
||||||
|
self.speed_ranges[speed] = SpeedRange(start, target)
|
||||||
|
|
||||||
|
def speed_to_homekit(self, speed):
|
||||||
|
"""Map Home Assistant speed state to HomeKit speed."""
|
||||||
|
speed_range = self.speed_ranges[speed]
|
||||||
|
return speed_range.target
|
||||||
|
|
||||||
|
def speed_to_states(self, speed):
|
||||||
|
"""Map HomeKit speed to Home Assistant speed state."""
|
||||||
|
for state, speed_range in reversed(self.speed_ranges.items()):
|
||||||
|
if speed_range.start <= speed:
|
||||||
|
return state
|
||||||
|
return list(self.speed_ranges.keys())[0]
|
||||||
|
|
||||||
|
|
||||||
def show_setup_message(hass, pincode):
|
def show_setup_message(hass, pincode):
|
||||||
"""Display persistent notification with setup information."""
|
"""Display persistent notification with setup information."""
|
||||||
pin = pincode.decode()
|
pin = pincode.decode()
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
"""Test different accessory types: Fans."""
|
"""Test different accessory types: Fans."""
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.fan import (
|
from homeassistant.components.fan import (
|
||||||
ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE,
|
ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_SPEED, ATTR_SPEED_LIST,
|
||||||
DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE)
|
DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SPEED_HIGH, SPEED_LOW,
|
||||||
|
SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED)
|
||||||
from homeassistant.components.homekit.const import ATTR_VALUE
|
from homeassistant.components.homekit.const import ATTR_VALUE
|
||||||
|
from homeassistant.components.homekit.util import HomeKitSpeedMapping
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF,
|
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON,
|
||||||
STATE_UNKNOWN)
|
STATE_UNKNOWN)
|
||||||
|
|
||||||
from tests.common import async_mock_service
|
from tests.common import async_mock_service
|
||||||
|
@ -39,6 +42,9 @@ async def test_fan_basic(hass, hk_driver, cls, events):
|
||||||
assert acc.category == 3 # Fan
|
assert acc.category == 3 # Fan
|
||||||
assert acc.char_active.value == 0
|
assert acc.char_active.value == 0
|
||||||
|
|
||||||
|
# If there are no speed_list values, then HomeKit speed is unsupported
|
||||||
|
assert acc.char_speed is None
|
||||||
|
|
||||||
await hass.async_add_job(acc.run)
|
await hass.async_add_job(acc.run)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert acc.char_active.value == 1
|
assert acc.char_active.value == 1
|
||||||
|
@ -155,3 +161,40 @@ async def test_fan_oscillate(hass, hk_driver, cls, events):
|
||||||
assert call_oscillate[1].data[ATTR_OSCILLATING] is True
|
assert call_oscillate[1].data[ATTR_OSCILLATING] is True
|
||||||
assert len(events) == 2
|
assert len(events) == 2
|
||||||
assert events[-1].data[ATTR_VALUE] is True
|
assert events[-1].data[ATTR_VALUE] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fan_speed(hass, hk_driver, cls, events):
|
||||||
|
"""Test fan with speed."""
|
||||||
|
entity_id = 'fan.demo'
|
||||||
|
speed_list = [SPEED_OFF, SPEED_LOW, SPEED_HIGH]
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {
|
||||||
|
ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED, ATTR_SPEED: SPEED_OFF,
|
||||||
|
ATTR_SPEED_LIST: speed_list})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None)
|
||||||
|
assert acc.char_speed.value == 0
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.run)
|
||||||
|
assert acc.speed_mapping.speed_ranges == \
|
||||||
|
HomeKitSpeedMapping(speed_list).speed_ranges
|
||||||
|
|
||||||
|
acc.speed_mapping.speed_to_homekit = Mock(return_value=42)
|
||||||
|
acc.speed_mapping.speed_to_states = Mock(return_value='ludicrous')
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {ATTR_SPEED: SPEED_HIGH})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc.speed_mapping.speed_to_homekit.assert_called_with(SPEED_HIGH)
|
||||||
|
assert acc.char_speed.value == 42
|
||||||
|
|
||||||
|
# Set from HomeKit
|
||||||
|
call_set_speed = async_mock_service(hass, DOMAIN, 'set_speed')
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_speed.client_update_value, 42)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc.speed_mapping.speed_to_states.assert_called_with(42)
|
||||||
|
assert call_set_speed[0]
|
||||||
|
assert call_set_speed[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert call_set_speed[0].data[ATTR_SPEED] == 'ludicrous'
|
||||||
|
assert len(events) == 1
|
||||||
|
assert events[-1].data[ATTR_VALUE] == 'ludicrous'
|
||||||
|
|
|
@ -3,15 +3,14 @@ import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.homekit.const import (
|
from homeassistant.components.homekit.const import (
|
||||||
CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF,
|
CONF_FEATURE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
|
||||||
FEATURE_PLAY_PAUSE, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER,
|
HOMEKIT_NOTIFY_ID, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER,
|
||||||
TYPE_SWITCH, TYPE_VALVE)
|
TYPE_SWITCH, TYPE_VALVE)
|
||||||
from homeassistant.components.homekit.util import (
|
from homeassistant.components.homekit.util import (
|
||||||
convert_to_float, density_to_air_quality, dismiss_setup_message,
|
HomeKitSpeedMapping, SpeedRange, convert_to_float, density_to_air_quality,
|
||||||
show_setup_message, temperature_to_homekit, temperature_to_states,
|
dismiss_setup_message, show_setup_message, temperature_to_homekit,
|
||||||
|
temperature_to_states, validate_entity_config as vec,
|
||||||
validate_media_player_features)
|
validate_media_player_features)
|
||||||
from homeassistant.components.homekit.util import validate_entity_config \
|
|
||||||
as vec
|
|
||||||
from homeassistant.components.persistent_notification import (
|
from homeassistant.components.persistent_notification import (
|
||||||
ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN)
|
ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -144,3 +143,58 @@ async def test_dismiss_setup_msg(hass):
|
||||||
assert call_dismiss_notification
|
assert call_dismiss_notification
|
||||||
assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == \
|
assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == \
|
||||||
HOMEKIT_NOTIFY_ID
|
HOMEKIT_NOTIFY_ID
|
||||||
|
|
||||||
|
|
||||||
|
def test_homekit_speed_mapping():
|
||||||
|
"""Test if the SpeedRanges from a speed_list are as expected."""
|
||||||
|
# A standard 2-speed fan
|
||||||
|
speed_mapping = HomeKitSpeedMapping(['off', 'low', 'high'])
|
||||||
|
assert speed_mapping.speed_ranges == {
|
||||||
|
'off': SpeedRange(0, 0),
|
||||||
|
'low': SpeedRange(100 / 3, 50),
|
||||||
|
'high': SpeedRange(200 / 3, 100),
|
||||||
|
}
|
||||||
|
|
||||||
|
# A standard 3-speed fan
|
||||||
|
speed_mapping = HomeKitSpeedMapping(['off', 'low', 'medium', 'high'])
|
||||||
|
assert speed_mapping.speed_ranges == {
|
||||||
|
'off': SpeedRange(0, 0),
|
||||||
|
'low': SpeedRange(100 / 4, 100 / 3),
|
||||||
|
'medium': SpeedRange(200 / 4, 200 / 3),
|
||||||
|
'high': SpeedRange(300 / 4, 100),
|
||||||
|
}
|
||||||
|
|
||||||
|
# a Dyson-like fan with 10 speeds
|
||||||
|
speed_mapping = HomeKitSpeedMapping([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||||
|
assert speed_mapping.speed_ranges == {
|
||||||
|
0: SpeedRange(0, 0),
|
||||||
|
1: SpeedRange(10, 100 / 9),
|
||||||
|
2: SpeedRange(20, 200 / 9),
|
||||||
|
3: SpeedRange(30, 300 / 9),
|
||||||
|
4: SpeedRange(40, 400 / 9),
|
||||||
|
5: SpeedRange(50, 500 / 9),
|
||||||
|
6: SpeedRange(60, 600 / 9),
|
||||||
|
7: SpeedRange(70, 700 / 9),
|
||||||
|
8: SpeedRange(80, 800 / 9),
|
||||||
|
9: SpeedRange(90, 100),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_speed_to_homekit():
|
||||||
|
"""Test speed conversion from HA to Homekit."""
|
||||||
|
speed_mapping = HomeKitSpeedMapping(['off', 'low', 'high'])
|
||||||
|
assert speed_mapping.speed_to_homekit('off') == 0
|
||||||
|
assert speed_mapping.speed_to_homekit('low') == 50
|
||||||
|
assert speed_mapping.speed_to_homekit('high') == 100
|
||||||
|
|
||||||
|
|
||||||
|
def test_speed_to_states():
|
||||||
|
"""Test speed conversion from Homekit to HA."""
|
||||||
|
speed_mapping = HomeKitSpeedMapping(['off', 'low', 'high'])
|
||||||
|
assert speed_mapping.speed_to_states(0) == 'off'
|
||||||
|
assert speed_mapping.speed_to_states(33) == 'off'
|
||||||
|
assert speed_mapping.speed_to_states(34) == 'low'
|
||||||
|
assert speed_mapping.speed_to_states(50) == 'low'
|
||||||
|
assert speed_mapping.speed_to_states(66) == 'low'
|
||||||
|
assert speed_mapping.speed_to_states(67) == 'high'
|
||||||
|
assert speed_mapping.speed_to_states(100) == 'high'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue