Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add service for sub profile switching #1813

Merged
merged 7 commits into from
Aug 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion custom_components/powercalc/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async def create_source_entity(entity_id: str, hass: HomeAssistant) -> SourceEnt
source_entity_domain,
unique_id,
get_wrapped_entity_name(
hass, entity_id, source_object_id, entity_entry, device_entry
hass, entity_id, source_object_id, entity_entry, device_entry,
),
supported_color_modes or [],
entity_entry,
Expand Down
1 change: 1 addition & 0 deletions custom_components/powercalc/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ class UnitPrefix(StrEnum):
SERVICE_INCREASE_DAILY_ENERGY = "increase_daily_energy"
SERVICE_CALIBRATE_UTILITY_METER = "calibrate_utility_meter"
SERVICE_CALIBRATE_ENERGY = "calibrate_energy"
SERVICE_SWITCH_SUB_PROFILE = "switch_sub_profile"

SIGNAL_POWER_SENSOR_STATE_CHANGE = "powercalc_power_sensor_state_change"

Expand Down
9 changes: 8 additions & 1 deletion custom_components/powercalc/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
SERVICE_INCREASE_DAILY_ENERGY,
SERVICE_RESET_ENERGY,
SERVICE_STOP_PLAYBOOK,
SERVICE_SWITCH_SUB_PROFILE,
CalculationStrategy,
PowercalcDiscoveryType,
SensorType,
Expand Down Expand Up @@ -494,6 +495,12 @@ def register_entity_services() -> None:
"async_stop_playbook",
)

platform.async_register_entity_service(
SERVICE_SWITCH_SUB_PROFILE,
{vol.Required("profile"): cv.string}, # type: ignore
"async_switch_sub_profile",
)


def convert_config_entry_to_sensor_config(config_entry: ConfigEntry) -> ConfigType:
"""Convert the config entry structure to the sensor config which we use to create the entities."""
Expand Down Expand Up @@ -723,7 +730,7 @@ async def create_individual_sensors(
hass,
sensor_config,
source_entity,
discovery_info,
config_entry,
)

entities_to_add.append(power_sensor)
Expand Down
2 changes: 1 addition & 1 deletion custom_components/powercalc/sensors/daily_energy.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ async def create_daily_fixed_energy_power_sensor(
unique_id,
)

return await create_virtual_power_sensor(hass, power_sensor_config, source_entity)
return await create_virtual_power_sensor(hass, power_sensor_config, source_entity, None)


class DailyEnergySensor(RestoreEntity, SensorEntity, EnergySensor):
Expand Down
151 changes: 93 additions & 58 deletions custom_components/powercalc/sensors/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_NAME,
CONF_UNIQUE_ID,
Expand Down Expand Up @@ -38,7 +39,7 @@
async_track_time_interval,
)
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.helpers.typing import ConfigType, StateType

from custom_components.powercalc.common import SourceEntity
from custom_components.powercalc.const import (
Expand Down Expand Up @@ -104,10 +105,10 @@


async def create_power_sensor(
hass: HomeAssistant,
sensor_config: dict,
source_entity: SourceEntity,
discovery_info: DiscoveryInfoType | None = None,
hass: HomeAssistant,
sensor_config: dict,
source_entity: SourceEntity,
config_entry: ConfigEntry | None,
) -> PowerSensor:
"""Create the power sensor based on powercalc sensor configuration."""
if CONF_POWER_SENSOR_ID in sensor_config:
Expand All @@ -118,15 +119,15 @@ async def create_power_sensor(
hass,
sensor_config,
source_entity,
discovery_info,
config_entry,
)


async def create_virtual_power_sensor(
hass: HomeAssistant,
sensor_config: ConfigType,
source_entity: SourceEntity,
discovery_info: DiscoveryInfoType | None = None,
hass: HomeAssistant,
sensor_config: ConfigType,
source_entity: SourceEntity,
config_entry: ConfigEntry | None,
) -> VirtualPowerSensor:
"""Create the power sensor entity."""
power_profile = None
Expand Down Expand Up @@ -177,7 +178,7 @@ async def create_virtual_power_sensor(
unique_id=unique_id,
)
entity_category: str | None = (
sensor_config.get(CONF_POWER_SENSOR_CATEGORY) or None
sensor_config.get(CONF_POWER_SENSOR_CATEGORY) or None
)

strategy = detect_calculation_strategy(sensor_config, power_profile)
Expand All @@ -198,9 +199,9 @@ async def create_virtual_power_sensor(
standby_power_on = Decimal(power_profile.standby_power_on)

if (
CONF_CALCULATION_ENABLED_CONDITION not in sensor_config
and power_profile is not None
and power_profile.calculation_enabled_condition
CONF_CALCULATION_ENABLED_CONDITION not in sensor_config
and power_profile is not None
and power_profile.calculation_enabled_condition
):
sensor_config[
CONF_CALCULATION_ENABLED_CONDITION
Expand Down Expand Up @@ -232,12 +233,13 @@ async def create_virtual_power_sensor(
update_frequency=sensor_config.get(CONF_FORCE_UPDATE_FREQUENCY), # type: ignore
multiply_factor=sensor_config.get(CONF_MULTIPLY_FACTOR),
multiply_factor_standby=sensor_config.get(CONF_MULTIPLY_FACTOR_STANDBY)
or False,
or False,
ignore_unavailable_state=sensor_config.get(CONF_IGNORE_UNAVAILABLE_STATE)
or False,
or False,
rounding_digits=sensor_config.get(CONF_POWER_SENSOR_PRECISION), # type: ignore
sensor_config=sensor_config,
power_profile=power_profile,
config_entry=config_entry,
)
await power_sensor.validate()
return power_sensor
Expand All @@ -251,8 +253,8 @@ async def create_virtual_power_sensor(


async def create_real_power_sensor(
hass: HomeAssistant,
sensor_config: dict,
hass: HomeAssistant,
sensor_config: dict,
) -> RealPowerSensor:
"""Create reference to an existing power sensor."""
power_sensor_id = sensor_config.get(CONF_POWER_SENSOR_ID)
Expand Down Expand Up @@ -300,24 +302,25 @@ class VirtualPowerSensor(SensorEntity, PowerSensor):
_attr_should_poll: bool = False

def __init__(
self,
hass: HomeAssistant,
calculation_strategy_factory: PowerCalculatorStrategyFactory,
calculation_strategy: CalculationStrategy,
entity_id: str,
entity_category: str | None,
name: str,
source_entity: SourceEntity,
unique_id: str | None,
standby_power: Decimal | Template,
standby_power_on: Decimal,
update_frequency: timedelta,
multiply_factor: float | None,
multiply_factor_standby: bool,
ignore_unavailable_state: bool,
rounding_digits: int,
sensor_config: dict,
power_profile: PowerProfile | None,
self,
hass: HomeAssistant,
calculation_strategy_factory: PowerCalculatorStrategyFactory,
calculation_strategy: CalculationStrategy,
entity_id: str,
entity_category: str | None,
name: str,
source_entity: SourceEntity,
unique_id: str | None,
standby_power: Decimal | Template,
standby_power_on: Decimal,
update_frequency: timedelta,
multiply_factor: float | None,
multiply_factor_standby: bool,
ignore_unavailable_state: bool,
rounding_digits: int,
sensor_config: dict,
power_profile: PowerProfile | None,
config_entry: ConfigEntry | None,
) -> None:
"""Initialize the sensor."""
self._calculation_strategy = calculation_strategy
Expand Down Expand Up @@ -349,21 +352,22 @@ def __init__(
self._power_profile = power_profile
self._sub_profile_selector: SubProfileSelector | None = None
if (
not self._ignore_unavailable_state
and self._sensor_config.get(CONF_UNAVAILABLE_POWER) is not None
not self._ignore_unavailable_state
and self._sensor_config.get(CONF_UNAVAILABLE_POWER) is not None
):
self._ignore_unavailable_state = True
self._standby_sensors: dict = hass.data[DOMAIN][DATA_STANDBY_POWER_SENSORS]
self.calculation_strategy_factory = calculation_strategy_factory
self._strategy_instance: PowerCalculationStrategyInterface | None = None
self._config_entry = config_entry

async def validate(self) -> None:
await self.ensure_strategy_instance()
assert self._strategy_instance is not None
await self._strategy_instance.validate_config()

async def ensure_strategy_instance(self) -> None:
if self._strategy_instance is None:
async def ensure_strategy_instance(self, recreate: bool = False) -> None:
if self._strategy_instance is None or recreate:
self._strategy_instance = await self.calculation_strategy_factory.create(
self._sensor_config,
self._calculation_strategy,
Expand Down Expand Up @@ -460,9 +464,9 @@ def async_update(event_time: datetime | None = None) -> None:
async_track_time_interval(self.hass, async_update, self._update_frequency)

async def _handle_source_entity_state_change(
self,
trigger_entity_id: str,
state: State | None,
self,
trigger_entity_id: str,
state: State | None,
) -> None:
"""Update power sensor based on new dependant entity state."""
self._standby_sensors.pop(self.entity_id, None)
Expand All @@ -482,7 +486,7 @@ async def _handle_source_entity_state_change(
self.async_write_ha_state()
return

self._switch_sub_profile_dynamically(state)
await self._switch_sub_profile_dynamically(state)
self._power = await self.calculate_power(state)

if self._power is not None:
Expand Down Expand Up @@ -523,9 +527,9 @@ async def calculate_power(self, state: State) -> Decimal | None:
"""Calculate power consumption using configured strategy."""
entity_state = state
if (
state.entity_id != self._source_entity.entity_id
and (entity_state := self.hass.states.get(self._source_entity.entity_id))
is None
state.entity_id != self._source_entity.entity_id
and (entity_state := self.hass.states.get(self._source_entity.entity_id))
is None
):
return None

Expand Down Expand Up @@ -557,22 +561,28 @@ async def calculate_power(self, state: State) -> Decimal | None:

return Decimal(power)

def _switch_sub_profile_dynamically(self, state: State) -> None:
async def _switch_sub_profile_dynamically(self, state: State) -> None:
"""Dynamically select a different sub profile depending on the entity state or attributes
Uses SubProfileSelect class which contains all the matching logic.
"""
if (
not self._power_profile
or not self._power_profile.sub_profile_select
or not self._sub_profile_selector
not self._power_profile
or not self._power_profile.sub_profile_select
or not self._sub_profile_selector
):
return

self._power_profile.select_sub_profile(
self._sub_profile_selector.select_sub_profile(state),
)
new_profile = self._sub_profile_selector.select_sub_profile(state)
await self._select_new_sub_profile(new_profile)

async def _select_new_sub_profile(self, profile: str) -> None:
if not self._power_profile or self._power_profile.sub_profile == profile:
return

self._power_profile.select_sub_profile(profile)
self._standby_power = Decimal(self._power_profile.standby_power)
self._standby_power_on = Decimal(self._power_profile.standby_power_on)
await self.ensure_strategy_instance(True)

async def calculate_standby_power(self, state: State) -> Decimal:
"""Calculate the power of the device in OFF state."""
Expand Down Expand Up @@ -663,15 +673,40 @@ async def async_stop_playbook(self) -> None:

await self._strategy_instance.stop_playbook()

async def async_switch_sub_profile(self, profile: str) -> None:
"""Switches to a new sub profile"""
if not self._power_profile or not self._power_profile.has_sub_profiles or self._power_profile.sub_profile_select:
raise HomeAssistantError(
"This is only supported for sensors having sub profiles, and no automatic profile selection",
)

if profile not in self._power_profile.get_sub_profiles():
raise HomeAssistantError(f"{profile} is not a possible sub profile")

await self._select_new_sub_profile(profile)

await self._handle_source_entity_state_change(
self._source_entity.entity_id,
self.hass.states.get(self._source_entity.entity_id),
)

# Persist the newly selected sub profile on the config entry
if self._config_entry:
new_model = f"{self._power_profile.model}/{profile}"
self.hass.config_entries.async_update_entry(
self._config_entry,
data={**self._config_entry.data, CONF_MODEL: new_model},
)


class RealPowerSensor(PowerSensor):
"""Contains a reference to an existing real power sensor entity."""

def __init__(
self,
entity_id: str,
device_id: str | None = None,
unique_id: str | None = None,
self,
entity_id: str,
device_id: str | None = None,
unique_id: str | None = None,
) -> None:
self.entity_id = entity_id
self._device_id = device_id
Expand Down
18 changes: 17 additions & 1 deletion custom_components/powercalc/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,20 @@ stop_playbook:
entity:
domain: sensor
integration: powercalc
device_class: power
device_class: power
switch_sub_profile:
name: Switch to another sub profile
description: Some profiles in the library has different sub profiles. This service allows you to switch to another one.
target:
entity:
domain: sensor
integration: powercalc
device_class: power
fields:
profile:
name: Sub profile
description: Define one of the possible sub profiles
example: "nigh_vision"
required: true
selector:
text:
10 changes: 10 additions & 0 deletions custom_components/powercalc/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,16 @@
"stop_playbook": {
"description": "Stop currently active playbook.",
"name": "Stop playbook"
},
"switch_sub_profile": {
"description": "Some profiles in the library has different sub profiles. This service allows you to switch to another one.",
"fields": {
"profile": {
"name": "Sub profile",
"description": "Define one of the possible sub profiles"
}
},
"name": "Switch to another sub profile"
}
}
}
Loading
Loading