Skip to content

Commit

Permalink
fix(controllers): fix get_entity_state called from initialize causes …
Browse files Browse the repository at this point in the history
…a crash in the app initialization if the state cannnot be read

related to #88
  • Loading branch information
xaviml committed Jun 13, 2020
1 parent 96ea71f commit 757366e
Show file tree
Hide file tree
Showing 14 changed files with 128 additions and 99 deletions.
41 changes: 33 additions & 8 deletions apps/controllerx/core/feature_support/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import List, Set, Union
from typing import List, Optional, Set, Union

from core.controller import Controller

SupportedFeatureNumber = Union[int, str]
Features = List[int]
Expand All @@ -17,12 +19,35 @@ def encode(supported_features: SupportedFeatures) -> int:
def decode(number: int, features: Features) -> SupportedFeatures:
return {number & feature for feature in features if number & feature != 0}

def __init__(self, number: SupportedFeatureNumber, features: Features) -> None:
parsed_number = int(number)
self.supported_features = FeatureSupport.decode(parsed_number, features)
def __init__(
self,
entity: Optional[str],
controller: Optional[Controller],
features: Features,
) -> None:
self.entity = entity
self.controller = controller
self._supported_features = None
self.features = features

async def supported_features(self):
if self._supported_features is None:
bitfield = await self.controller.get_entity_state(
self.entity, attribute="supported_features"
)
if bitfield is not None:
bitfield = int(bitfield)
self._supported_features = FeatureSupport.decode(
bitfield, self.features
)
else:
raise ValueError(
f"`supported_features` could not be read from `{self.entity}`. Entity might not be available."
)
return self._supported_features

def is_supported(self, feature: int) -> bool:
return feature in self.supported_features
async def is_supported(self, feature: int) -> bool:
return feature in await self.supported_features()

def not_supported(self, feature: int) -> bool:
return feature not in self.supported_features
async def not_supported(self, feature: int) -> bool:
return feature not in await self.supported_features()
9 changes: 6 additions & 3 deletions apps/controllerx/core/feature_support/cover.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from core.feature_support import FeatureSupport, SupportedFeatureNumber
from typing import Optional
from core.controller import Controller
from core.feature_support import FeatureSupport

SUPPORT_OPEN = 1
SUPPORT_CLOSE = 2
Expand All @@ -21,9 +23,10 @@ class CoverSupport(FeatureSupport):
STOP_TILT = 64
SET_TILT_POSITION = 128

def __init__(self, number: SupportedFeatureNumber) -> None:
def __init__(self, entity: Optional[str], controller: Optional[Controller]) -> None:
super().__init__(
number,
entity,
controller,
[
CoverSupport.OPEN,
CoverSupport.CLOSE,
Expand Down
9 changes: 6 additions & 3 deletions apps/controllerx/core/feature_support/light.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from core.feature_support import FeatureSupport, SupportedFeatureNumber
from typing import Optional
from core.controller import Controller
from core.feature_support import FeatureSupport


class LightSupport(FeatureSupport):
Expand All @@ -10,9 +12,10 @@ class LightSupport(FeatureSupport):
TRANSITION = 32
WHITE_VALUE = 128

def __init__(self, number: SupportedFeatureNumber) -> None:
def __init__(self, entity: Optional[str], controller: Optional[Controller]) -> None:
super().__init__(
number,
entity,
controller,
[
LightSupport.BRIGHTNESS,
LightSupport.COLOR_TEMP,
Expand Down
9 changes: 6 additions & 3 deletions apps/controllerx/core/feature_support/media_player.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from core.feature_support import FeatureSupport, SupportedFeatureNumber
from typing import Optional
from core.controller import Controller
from core.feature_support import FeatureSupport


class MediaPlayerSupport(FeatureSupport):
Expand All @@ -19,9 +21,10 @@ class MediaPlayerSupport(FeatureSupport):
SHUFFLE_SET = 32768
SELECT_SOUND_MODE = 65536

def __init__(self, number: SupportedFeatureNumber) -> None:
def __init__(self, entity: Optional[str], controller: Optional[Controller]) -> None:
super().__init__(
number,
entity,
controller,
[
MediaPlayerSupport.PAUSE,
MediaPlayerSupport.SEEK,
Expand Down
13 changes: 5 additions & 8 deletions apps/controllerx/core/type/cover_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ async def initialize(self) -> None:
raise ValueError("`open_position` must be higher than `close_position`")
await self.check_domain(self.cover)

bitfield = await self.get_entity_state(
self.cover, attribute="supported_features"
)
self.supported_features = CoverSupport(bitfield)
self.supported_features = CoverSupport(self.cover, self)

await super().initialize()

Expand All @@ -46,13 +43,13 @@ def get_type_actions_mapping(self) -> TypeActionsMapping:

@action
async def open(self) -> None:
if self.supported_features.is_supported(CoverSupport.SET_COVER_POSITION):
if await self.supported_features.is_supported(CoverSupport.SET_COVER_POSITION):
await self.call_service(
"cover/set_cover_position",
entity_id=self.cover,
position=self.open_position,
)
elif self.supported_features.is_supported(CoverSupport.OPEN):
elif await self.supported_features.is_supported(CoverSupport.OPEN):
await self.call_service("cover/open_cover", entity_id=self.cover)
else:
self.log(
Expand All @@ -63,13 +60,13 @@ async def open(self) -> None:

@action
async def close(self) -> None:
if self.supported_features.is_supported(CoverSupport.SET_COVER_POSITION):
if await self.supported_features.is_supported(CoverSupport.SET_COVER_POSITION):
await self.call_service(
"cover/set_cover_position",
entity_id=self.cover,
position=self.close_position,
)
elif self.supported_features.is_supported(CoverSupport.CLOSE):
elif await self.supported_features.is_supported(CoverSupport.CLOSE):
await self.call_service("cover/close_cover", entity_id=self.cover)
else:
self.log(
Expand Down
22 changes: 10 additions & 12 deletions apps/controllerx/core/type/light_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,7 @@ async def initialize(self) -> None:
"add_transition_turn_toggle", True
)

bitfield = await self.get_entity_state(
self.light["name"], attribute="supported_features"
)

self.supported_features = LightSupport(bitfield)
self.supported_features = LightSupport(self.light["name"], self)
await super().initialize()

def get_domain(self) -> str:
Expand Down Expand Up @@ -274,7 +270,7 @@ async def call_light_service(
if "transition" not in attributes:
attributes["transition"] = self.transition / 1000
if (
self.supported_features.not_supported(LightSupport.TRANSITION)
await self.supported_features.not_supported(LightSupport.TRANSITION)
or not self.add_transition
or (turned_toggle and not self.add_transition_turn_toggle)
):
Expand Down Expand Up @@ -324,7 +320,7 @@ async def on_min(self, attribute: str, light_on: bool = None) -> None:
async def sync(self) -> None:
attributes: Dict[Any, Any] = {}
try:
color_attribute = self.get_attribute(LightController.ATTRIBUTE_COLOR)
color_attribute = await self.get_attribute(LightController.ATTRIBUTE_COLOR)
if color_attribute == LightController.ATTRIBUTE_COLOR_TEMP:
attributes[color_attribute] = 370 # 2700K light
else:
Expand All @@ -334,12 +330,14 @@ async def sync(self) -> None:
self.log("⚠️ `sync` action will only change brightness", level="WARNING")
await self.on(**attributes, brightness=self.max_brightness)

def get_attribute(self, attribute: str) -> str:
async def get_attribute(self, attribute: str) -> str:
if attribute == LightController.ATTRIBUTE_COLOR:
if self.light["color_mode"] == "auto":
if self.supported_features.is_supported(LightSupport.COLOR):
if await self.supported_features.is_supported(LightSupport.COLOR):
return LightController.ATTRIBUTE_XY_COLOR
elif self.supported_features.is_supported(LightSupport.COLOR_TEMP):
elif await self.supported_features.is_supported(
LightSupport.COLOR_TEMP
):
return LightController.ATTRIBUTE_COLOR_TEMP
else:
raise ValueError(
Expand Down Expand Up @@ -404,7 +402,7 @@ async def before_action(self, action: str, *args, **kwargs) -> bool:

@action
async def click(self, attribute: str, direction: str) -> None:
attribute = self.get_attribute(attribute)
attribute = await self.get_attribute(attribute)
self.value_attribute = await self.get_value_attribute(attribute, direction)
await self.change_light_state(
self.value_attribute,
Expand All @@ -416,7 +414,7 @@ async def click(self, attribute: str, direction: str) -> None:

@action
async def hold(self, attribute: str, direction: str) -> None:
attribute = self.get_attribute(attribute)
attribute = await self.get_attribute(attribute)
self.value_attribute = await self.get_value_attribute(attribute, direction)
direction = self.automatic_steppers[attribute].get_direction(
self.value_attribute, direction
Expand Down
8 changes: 2 additions & 6 deletions apps/controllerx/core/type/media_player_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ async def initialize(self) -> None:
self.volume_stepper = MinMaxStepper(0, 1, volume_steps)
self.volume_level = 0.0

bitfield = await self.get_entity_state(
self.media_player, attribute="supported_features"
)
self.supported_features = MediaPlayerSupport(bitfield)
self.supported_features = MediaPlayerSupport(self.media_player, self)
await super().initialize()

def get_domain(self) -> str:
Expand Down Expand Up @@ -105,8 +102,7 @@ async def prepare_volume_change(self) -> None:
self.volume_level = volume_level

async def volume_change(self, direction: str) -> bool:

if self.supported_features.is_supported(MediaPlayerSupport.VOLUME_SET):
if await self.supported_features.is_supported(MediaPlayerSupport.VOLUME_SET):
self.volume_level, exceeded = self.volume_stepper.step(
self.volume_level, direction
)
Expand Down
15 changes: 9 additions & 6 deletions tests/core/feature_support/cover_support_test.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from core.feature_support.cover import CoverSupport
import pytest
from core.feature_support import FeatureSupport


@pytest.mark.parametrize(
"number, expected_supported_features",
[
("1", {CoverSupport.OPEN,},),
(1, {CoverSupport.OPEN,},),
(
"15",
15,
{
CoverSupport.OPEN,
CoverSupport.CLOSE,
Expand All @@ -16,7 +17,7 @@
},
),
(
"149",
149,
{
CoverSupport.SET_TILT_POSITION,
CoverSupport.OPEN_TILT,
Expand All @@ -25,9 +26,11 @@
},
),
(0, set()),
("0", set()),
],
)
def test_init(number, expected_supported_features):
cover_support = CoverSupport(number)
assert cover_support.supported_features == expected_supported_features
cover_support = CoverSupport(None, None)
cover_support._supported_features = FeatureSupport.decode(
number, cover_support.features
)
assert cover_support._supported_features == expected_supported_features
30 changes: 10 additions & 20 deletions tests/core/feature_support/feature_support_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,6 @@ def test_encode(supported_features, expected_number):
assert expected_number == number


@pytest.mark.parametrize(
"number, features, expected_features",
[
("15", [1, 2, 4, 8, 16, 32, 64], {1, 2, 4, 8}),
(16, [1, 2, 4, 8, 16, 32, 64], {16}),
("31", [1, 2, 4, 8, 16, 64], {1, 2, 4, 8, 16}),
(70, [1, 2, 4, 8, 16, 64], {2, 4, 64}),
],
)
def test_init(number, features, expected_features):
feature_support = FeatureSupport(number, features)
assert feature_support.supported_features == expected_features


@pytest.mark.parametrize(
"number, features, feature, is_supported",
[
Expand All @@ -55,9 +41,11 @@ def test_init(number, features, expected_features):
(9, [1, 2, 4, 8, 16, 64], 8, True),
],
)
def test_is_supported(number, features, feature, is_supported):
feature_support = FeatureSupport(number, features)
is_supported = feature_support.is_supported(feature)
@pytest.mark.asyncio
async def test_is_supported(number, features, feature, is_supported):
feature_support = FeatureSupport(None, None, features)
feature_support._supported_features = FeatureSupport.decode(number, features)
is_supported = await feature_support.is_supported(feature)
assert is_supported == is_supported


Expand All @@ -71,7 +59,9 @@ def test_is_supported(number, features, feature, is_supported):
(9, [1, 2, 4, 8, 16, 64], 8, False),
],
)
def test_not_supported(number, features, feature, is_supported):
feature_support = FeatureSupport(number, features)
is_supported = feature_support.is_supported(feature)
@pytest.mark.asyncio
async def test_not_supported(number, features, feature, is_supported):
feature_support = FeatureSupport(None, None, features)
feature_support._supported_features = FeatureSupport.decode(number, features)
is_supported = await feature_support.not_supported(feature)
assert is_supported == is_supported
15 changes: 9 additions & 6 deletions tests/core/feature_support/light_support_test.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from core.feature_support.light import LightSupport
import pytest
from core.feature_support import FeatureSupport


@pytest.mark.parametrize(
"number, expected_supported_features",
[
("1", {LightSupport.BRIGHTNESS,},),
(1, {LightSupport.BRIGHTNESS,},),
(
"57",
57,
{
LightSupport.BRIGHTNESS,
LightSupport.FLASH,
Expand All @@ -16,7 +17,7 @@
},
),
(
"149",
149,
{
LightSupport.BRIGHTNESS,
LightSupport.EFFECT,
Expand All @@ -25,9 +26,11 @@
},
),
(0, set()),
("0", set()),
],
)
def test_init(number, expected_supported_features):
light_support = LightSupport(number)
assert light_support.supported_features == expected_supported_features
light_support = LightSupport(None, None)
light_support._supported_features = FeatureSupport.decode(
number, light_support.features
)
assert light_support._supported_features == expected_supported_features
Loading

0 comments on commit 757366e

Please sign in to comment.