Skip to content

Commit

Permalink
Allow users to set a fixed power value for Ikea Smart Outlet (#1325)
Browse files Browse the repository at this point in the history
  • Loading branch information
bramstroker committed Dec 3, 2022
1 parent 155b0b1 commit 81f445c
Show file tree
Hide file tree
Showing 23 changed files with 290 additions and 145 deletions.
8 changes: 1 addition & 7 deletions custom_components/powercalc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,6 @@ async def start_discovery(self):
)
continue

if power_profile.is_additional_configuration_required:
_LOGGER.warning(
f"{entity_entry.entity_id}: Model found in database, but needs additional manual configuration to be loaded"
)
continue

if not power_profile.is_entity_domain_supported(source_entity.domain):
continue

Expand Down Expand Up @@ -422,7 +416,7 @@ def _init_entity_discovery(
)

# Code below if for legacy discovery routine, will be removed somewhere in the future
if power_profile and not power_profile.has_sub_profiles:
if power_profile and not power_profile.is_additional_configuration_required:
discovery_info = {
CONF_ENTITY_ID: source_entity.entity_id,
DISCOVERY_SOURCE_ENTITY: source_entity,
Expand Down
37 changes: 20 additions & 17 deletions custom_components/powercalc/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,13 +439,7 @@ async def async_step_library(self, user_input: dict[str, str] = None) -> FlowRes
CONF_MODEL: self.power_profile.model,
}
)
if (
self.power_profile.has_sub_profiles
and not self.power_profile.sub_profile_select
):
return await self.async_step_sub_profile()

return await self.async_step_power_advanced()
return await self.async_step_post_library(user_input)

return await self.async_step_manufacturer()

Expand Down Expand Up @@ -497,11 +491,8 @@ async def async_step_model(self, user_input: dict[str, str] = None) -> FlowResul
self.sensor_config.get(CONF_MODEL),
)
)
if profile.has_sub_profiles:
return await self.async_step_sub_profile()
errors = await self.validate_strategy_config()
if not errors:
return await self.async_step_power_advanced()
self.power_profile = profile
return await self.async_step_post_library()

return self.async_show_form(
step_id="model",
Expand All @@ -514,6 +505,19 @@ async def async_step_model(self, user_input: dict[str, str] = None) -> FlowResul
errors=errors,
)

async def async_step_post_library(self, user_input: dict[str, str] = None):
"""Handles the logic after the user either selected manufacturer/model himself or confirmed autodiscovered"""
if (
self.power_profile.has_sub_profiles
and not self.power_profile.sub_profile_select
):
return await self.async_step_sub_profile()

if self.power_profile.needs_fixed_config:
return await self.async_step_fixed()

return await self.async_step_power_advanced()

async def async_step_sub_profile(
self, user_input: dict[str, str] = None
) -> FlowResult:
Expand Down Expand Up @@ -549,9 +553,9 @@ async def async_step_power_advanced(
)

async def validate_strategy_config(self) -> dict:
strategy_name = self.sensor_config.get(CONF_MODE)
strategy_name = self.sensor_config.get(CONF_MODE) or self.power_profile.supported_strategies[0]
strategy = await _create_strategy_object(
self.hass, strategy_name, self.sensor_config, self.source_entity
self.hass, strategy_name, self.sensor_config, self.source_entity, self.power_profile
)
try:
await strategy.validate_config()
Expand Down Expand Up @@ -687,12 +691,11 @@ def build_options_schema(self) -> vol.Schema:


async def _create_strategy_object(
hass: HomeAssistant, strategy: str, config: dict, source_entity: SourceEntity
hass: HomeAssistant, strategy: str, config: dict, source_entity: SourceEntity, power_profile: PowerProfile | None = None
) -> PowerCalculationStrategyInterface:
"""Create the calculation strategy object"""
factory = PowerCalculatorStrategyFactory(hass)
power_profile: PowerProfile | None = None
if strategy == CalculationStrategy.LUT:
if power_profile is None and CONF_MANUFACTURER in config:
power_profile = await ProfileLibrary.factory(hass).get_profile(
ModelInfo(config.get(CONF_MANUFACTURER), config.get(CONF_MODEL))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
"measure_device": "Shelly Plug S",
"name": "TRADFRI control outlet",
"standby_power": 0.4,
"standby_power_on": 0.8,
"sensor_config": {
"power_sensor_naming": "{} Device Power",
"energy_sensor_naming": "{} Device Energy"
},
"device_type": "smart_switch",
"supported_modes": [
"fixed"
],
"fixed_config": {
"power": 0.8
}
]
}
4 changes: 0 additions & 4 deletions custom_components/powercalc/data/model_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,6 @@
"type": "string",
"description": "Use LUT data files from another model"
},
"requires_additional_configuration": {
"type": "boolean",
"description": "Defines whether this model needs additional configuration by the user. Used for smart switches and lights where we can only know the standby power, but not the power of connected light bulb"
},
"device_type": {
"type": "string",
"enum": [
Expand Down
2 changes: 1 addition & 1 deletion custom_components/powercalc/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ModelNotSupported(StrategyConfigurationError):
"""Raised when model is not supported."""


class UnsupportedMode(PowercalcSetupError):
class UnsupportedStrategy(PowercalcSetupError):
"""Mode not supported."""


Expand Down
45 changes: 31 additions & 14 deletions custom_components/powercalc/power_profile/power_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from homeassistant.helpers.typing import ConfigType

from ..common import SourceEntity
from ..const import CalculationStrategy
from ..errors import ModelNotSupported, PowercalcSetupError, UnsupportedMode
from ..const import CalculationStrategy, CONF_POWER
from ..errors import ModelNotSupported, PowercalcSetupError, UnsupportedStrategy

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -102,7 +102,7 @@ def standby_power_on(self) -> float:
return self._json_data.get("standby_power_on") or 0

@property
def supported_modes(self) -> list[CalculationStrategy]:
def supported_strategies(self) -> list[CalculationStrategy]:
return self._json_data.get("supported_modes") or [CalculationStrategy.LUT]

@property
Expand All @@ -120,32 +120,49 @@ def aliases(self) -> list[str]:
@property
def linear_mode_config(self) -> ConfigType | None:
"""Get configuration to setup linear strategy"""
if not self.is_mode_supported(CalculationStrategy.LINEAR):
raise UnsupportedMode(
f"Mode linear is not supported by model: {self._model}"
if not self.is_strategy_supported(CalculationStrategy.LINEAR):
raise UnsupportedStrategy(
f"Strategy linear is not supported by model: {self._model}"
)
return self._json_data.get("linear_config")

@property
def fixed_mode_config(self) -> ConfigType | None:
def fixed_mode_config(self) -> ConfigType:
"""Get configuration to setup fixed strategy"""
if not self.is_mode_supported(CalculationStrategy.FIXED):
raise UnsupportedMode(
f"Mode fixed is not supported by model: {self._model}"
if not self.is_strategy_supported(CalculationStrategy.FIXED):
raise UnsupportedStrategy(
f"Strategy fixed is not supported by model: {self._model}"
)
return self._json_data.get("fixed_config")
fixed_config = self._json_data.get("fixed_config")
if fixed_config is None and self.standby_power_on:
fixed_config = {CONF_POWER: 0}
return fixed_config

@property
def sensor_config(self) -> ConfigType:
"""Additional sensor configuration"""
return self._json_data.get("sensor_config") or {}

def is_mode_supported(self, mode: str) -> bool:
return mode in self.supported_modes
def is_strategy_supported(self, mode: CalculationStrategy) -> bool:
"""Whether a certain calculation strategy is supported by this profile"""
return mode in self.supported_strategies

@property
def is_additional_configuration_required(self) -> bool:
return self._json_data.get("requires_additional_configuration") or False
"""Checks if the power profile can be setup without any additional user configuration."""
if self.has_sub_profiles and self.sub_profile is None:
return True
if self.needs_fixed_config:
return True
return False

@property
def needs_fixed_config(self) -> bool:
"""
Used for smart switches which only provides standby power values.
This indicates the user must supply the power values in the config flow
"""
return self.is_strategy_supported(CalculationStrategy.FIXED) and not self._json_data.get("fixed_config")

@property
def device_type(self) -> DeviceType:
Expand Down
8 changes: 4 additions & 4 deletions custom_components/powercalc/sensors/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
OFF_STATES,
CalculationStrategy,
)
from ..errors import ModelNotSupported, StrategyConfigurationError, UnsupportedMode
from ..errors import ModelNotSupported, StrategyConfigurationError, UnsupportedStrategy
from ..power_profile.model_discovery import get_power_profile
from ..power_profile.power_profile import PowerProfile, SubProfileSelector
from ..strategy.factory import PowerCalculatorStrategyFactory
Expand Down Expand Up @@ -147,7 +147,7 @@ async def create_virtual_power_sensor(
sensor_config, strategy, power_profile, source_entity
)
await calculation_strategy.validate_config()
except (StrategyConfigurationError, UnsupportedMode) as err:
except (StrategyConfigurationError, UnsupportedStrategy) as err:
_LOGGER.error(
"%s: Skipping sensor setup: %s",
source_entity.entity_id,
Expand Down Expand Up @@ -244,9 +244,9 @@ def select_calculation_strategy(
return CalculationStrategy.WLED

if power_profile:
return power_profile.supported_modes[0]
return power_profile.supported_strategies[0]

raise UnsupportedMode(
raise UnsupportedStrategy(
"Cannot select a strategy (LINEAR, FIXED or LUT, WLED), supply it in the config. See the readme"
)

Expand Down
4 changes: 2 additions & 2 deletions custom_components/powercalc/strategy/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
CONF_WLED,
CalculationStrategy,
)
from ..errors import StrategyConfigurationError, UnsupportedMode
from ..errors import StrategyConfigurationError, UnsupportedStrategy
from ..power_profile.power_profile import PowerProfile
from .fixed import FixedStrategy
from .linear import LinearStrategy
Expand Down Expand Up @@ -49,7 +49,7 @@ def create(
if strategy == CalculationStrategy.WLED:
return self._create_wled(source_entity, config)

raise UnsupportedMode("Invalid calculation mode", strategy)
raise UnsupportedStrategy("Invalid calculation mode", strategy)

def _create_linear(
self, source_entity: SourceEntity, config: dict, power_profile: PowerProfile
Expand Down
2 changes: 1 addition & 1 deletion tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def create_discoverable_light(
return light


async def run_powercalc_setup_yaml_config(
async def run_powercalc_setup(
hass: HomeAssistant,
sensor_config: list[ConfigType] | ConfigType,
domain_config: ConfigType | None = None,
Expand Down
4 changes: 2 additions & 2 deletions tests/power_profile/device_types/test_infrared_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
CONF_MODEL,
)
from custom_components.test.light import MockLight
from tests.common import get_test_profile_dir, run_powercalc_setup_yaml_config
from tests.common import get_test_profile_dir, run_powercalc_setup

from ...common import create_mock_light_entity

Expand All @@ -33,7 +33,7 @@ async def test_infrared_light(hass: HomeAssistant):

await create_mock_light_entity(hass, light_mock)

await run_powercalc_setup_yaml_config(
await run_powercalc_setup(
hass,
{
CONF_ENTITY_ID: light_id,
Expand Down
4 changes: 2 additions & 2 deletions tests/power_profile/device_types/test_media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
CONF_MANUFACTURER,
CONF_MODEL,
)
from tests.common import get_test_profile_dir, run_powercalc_setup_yaml_config
from tests.common import get_test_profile_dir, run_powercalc_setup


async def test_media_player(hass: HomeAssistant):
Expand Down Expand Up @@ -47,7 +47,7 @@ async def test_media_player(hass: HomeAssistant):

power_sensor_id = "sensor.nest_mini_power"

await run_powercalc_setup_yaml_config(
await run_powercalc_setup(
hass,
{
CONF_ENTITY_ID: entity_id,
Expand Down
Loading

0 comments on commit 81f445c

Please sign in to comment.