Skip to content

Commit

Permalink
Feat/cycle action (#354)
Browse files Browse the repository at this point in the history
* refactor(stepper): refactor stepper to be more isolated

this will enable the integration of a bouncing stepper

* refactor(stepper): create StepperOutput with next_direction

* feat(light_controller): add click/hold modes

related to #204
  • Loading branch information
xaviml committed Sep 4, 2021
1 parent 0eac9a8 commit 3f13f04
Show file tree
Hide file tree
Showing 30 changed files with 861 additions and 475 deletions.
4 changes: 4 additions & 0 deletions apps/controllerx/cx_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
CustomActions = Union[List[CustomAction], CustomAction]
CustomActionsMapping = Dict[ActionEvent, CustomActions]

Number = Union[int, float]


class Light:
ON = "on"
Expand All @@ -34,6 +36,7 @@ class Light:
SET_HALF_WHITE_VALUE = "set_half_white_value"
SET_HALF_COLOR_TEMP = "set_half_color_temp"
SYNC = "sync"
CLICK = "click"
CLICK_BRIGHTNESS_UP = "click_brightness_up"
CLICK_BRIGHTNESS_DOWN = "click_brightness_down"
CLICK_WHITE_VALUE_UP = "click_white_value_up"
Expand All @@ -44,6 +47,7 @@ class Light:
CLICK_COLOR_TEMP_DOWN = "click_colortemp_down"
CLICK_XY_COLOR_UP = "click_xycolor_up"
CLICK_XY_COLOR_DOWN = "click_xycolor_down"
HOLD = "hold"
HOLD_BRIGHTNESS_UP = "hold_brightness_up"
HOLD_BRIGHTNESS_DOWN = "hold_brightness_down"
HOLD_BRIGHTNESS_TOGGLE = "hold_brightness_toggle"
Expand Down
9 changes: 7 additions & 2 deletions apps/controllerx/cx_core/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,16 @@ def filter_actions(
if key in allowed_actions
}

def get_option(self, value: str, options: List[str]) -> str:
def get_option(
self, value: str, options: List[str], ctx: Optional[str] = None
) -> str:
if value in options:
return value
else:
raise ValueError(f"{value} is not an option. The options are {options}")
raise ValueError(
f"{f'{ctx} - ' if ctx is not None else ''}`{value}` is not an option. "
f"The options are {options}"
)

def parse_integration(
self, integration: Union[str, Dict[str, Any], Any]
Expand Down
81 changes: 51 additions & 30 deletions apps/controllerx/cx_core/stepper/__init__.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,90 @@
import abc
from typing import Tuple, Union
from typing import Optional

from attr import dataclass
from cx_const import Number


class MinMax:
def __init__(self, min_: float, max_: float, margin=0.05) -> None:
self._min = min_
self._max = max_
self.margin_dist = (max_ - min_) * margin
def __init__(self, min: Number, max: Number, margin: float = 0.05) -> None:
self._min = min
self._max = max
self.margin_dist = (max - min) * margin

@property
def min(self) -> float:
def min(self) -> Number:
return self._min

@property
def max(self) -> float:
def max(self) -> Number:
return self._max

def is_min(self, value: float) -> bool:
def is_min(self, value: Number) -> bool:
return self._min == value

def is_max(self, value: float) -> bool:
def is_max(self, value: Number) -> bool:
return self._max == value

def is_between(self, value: float) -> bool:
def is_between(self, value: Number) -> bool:
return self._min < value < self._max

def in_min_boundaries(self, value: float) -> bool:
def in_min_boundaries(self, value: Number) -> bool:
return self._min <= value <= (self._min + self.margin_dist)

def in_max_boundaries(self, value: float) -> bool:
def in_max_boundaries(self, value: Number) -> bool:
return (self._max - self.margin_dist) <= value <= self._max

def clip(self, value: float) -> float:
def clip(self, value: Number) -> Number:
return max(self._min, min(value, self._max))

def __repr__(self) -> str:
return f"MinMax({self.min}, {self.max})"


@dataclass
class StepperOutput:
next_value: Number
next_direction: Optional[str]

@property
def exceeded(self) -> bool:
return self.next_direction is None


class Stepper(abc.ABC):
UP = "up"
DOWN = "down"
TOGGLE_UP = "toggle_up"
TOGGLE_DOWN = "toggle_down"
TOGGLE = "toggle"
sign_mapping = {UP: 1, DOWN: -1, TOGGLE_UP: 1, TOGGLE_DOWN: -1}
sign_mapping = {UP: 1, DOWN: -1}

def __init__(self) -> None:
self.previous_direction = Stepper.TOGGLE_DOWN
previous_direction: str = DOWN
min_max: MinMax
steps: Number

def get_direction(self, value: float, direction: str) -> str:
@staticmethod
def invert_direction(direction: str) -> str:
return Stepper.UP if direction == Stepper.DOWN else Stepper.DOWN

@staticmethod
def sign(direction: str) -> int:
return Stepper.sign_mapping[direction]

def __init__(self, min_max: MinMax, steps: Number) -> None:
self.min_max = min_max
self.steps = steps

def get_direction(self, value: Number, direction: str) -> str:
if direction == Stepper.TOGGLE:
direction = (
Stepper.TOGGLE_UP
if self.previous_direction == Stepper.TOGGLE_DOWN
else Stepper.TOGGLE_DOWN
)
direction = Stepper.invert_direction(self.previous_direction)
self.previous_direction = direction
return direction

def sign(self, direction: str) -> int:
return Stepper.sign_mapping[direction]

@abc.abstractmethod
def step(self, value: float, direction: str) -> Tuple[Union[int, float], bool]:
def step(self, value: Number, direction: str) -> StepperOutput:
"""
This function updates the value according to the steps
that needs to take and returns the new value and True
if the step exceeds the boundaries.
that needs to take and returns the new value together with
the new direction it will need to go. If next_direction is
None, the loop will stop executing.
"""
raise NotImplementedError
20 changes: 20 additions & 0 deletions apps/controllerx/cx_core/stepper/bounce_stepper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from cx_const import Number
from cx_core.stepper import Stepper, StepperOutput


class BounceStepper(Stepper):
def step(self, value: Number, direction: str) -> StepperOutput:
value = self.min_max.clip(value)
sign = Stepper.sign(direction)
max_ = self.min_max.max
min_ = self.min_max.min
step = (max_ - min_) / self.steps

new_value = value + sign * step
if self.min_max.is_between(new_value):
return StepperOutput(round(new_value, 3), next_direction=direction)
else:
new_value = 2 * self.min_max.clip(new_value) - new_value
return StepperOutput(
round(new_value, 3), next_direction=Stepper.invert_direction(direction)
)
18 changes: 0 additions & 18 deletions apps/controllerx/cx_core/stepper/circular_stepper.py

This file was deleted.

18 changes: 18 additions & 0 deletions apps/controllerx/cx_core/stepper/index_loop_stepper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from cx_const import Number
from cx_core.stepper import MinMax, Stepper, StepperOutput


class IndexLoopStepper(Stepper):
def __init__(self, size: int) -> None:
super().__init__(MinMax(0, size - 1), size)

def step(self, value: Number, direction: str) -> StepperOutput:
value = self.min_max.clip(value)
sign = self.sign(direction)
# We add +1 to make the max be included
max_ = int(self.min_max.max) + 1
min_ = int(self.min_max.min)
step = (max_ - min_) // self.steps

new_value = (int(value) + step * sign) % (max_ - min_) + min_
return StepperOutput(new_value, next_direction=direction)
16 changes: 16 additions & 0 deletions apps/controllerx/cx_core/stepper/loop_stepper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from cx_const import Number
from cx_core.stepper import Stepper, StepperOutput


class LoopStepper(Stepper):
def step(self, value: Number, direction: str) -> StepperOutput:
value = self.min_max.clip(value)
sign = Stepper.sign(direction)
# We add +1 to include `max`
max_ = self.min_max.max
min_ = self.min_max.min
step = (max_ - min_) / self.steps

new_value = (((value + step * sign) - min_) % (max_ - min_)) + min_
new_value = round(new_value, 3)
return StepperOutput(new_value, next_direction=direction)
39 changes: 0 additions & 39 deletions apps/controllerx/cx_core/stepper/minmax_stepper.py

This file was deleted.

29 changes: 29 additions & 0 deletions apps/controllerx/cx_core/stepper/stop_stepper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from cx_const import Number
from cx_core.stepper import Stepper, StepperOutput


class StopStepper(Stepper):
def get_direction(self, value: Number, direction: str) -> str:
value = self.min_max.clip(value)
if direction == Stepper.TOGGLE and self.min_max.in_min_boundaries(value):
self.previous_direction = Stepper.UP
return self.previous_direction
if direction == Stepper.TOGGLE and self.min_max.in_max_boundaries(value):
self.previous_direction = Stepper.DOWN
return self.previous_direction
return super().get_direction(value, direction)

def step(self, value: Number, direction: str) -> StepperOutput:
value = self.min_max.clip(value)
sign = Stepper.sign(direction)
max_ = self.min_max.max
min_ = self.min_max.min
step = (max_ - min_) / self.steps

new_value = value + sign * step
new_value = round(new_value, 3)
if self.min_max.is_between(new_value):
return StepperOutput(new_value, next_direction=direction)
else:
new_value = self.min_max.clip(new_value)
return StepperOutput(new_value, next_direction=None)

0 comments on commit 3f13f04

Please sign in to comment.