Skip to content

Commit

Permalink
Set field in config flow to same value as global powercalc configurat…
Browse files Browse the repository at this point in the history
…ion (#1870)

* Set field in config flow to same value as global powercalc configuration

* Fix tests

* Fix for library flow

* Implement service for changing gui configuration

* Add documentation

* Remove return
  • Loading branch information
bramstroker authored Sep 17, 2023
1 parent e076f7b commit 3584079
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 61 deletions.
131 changes: 73 additions & 58 deletions custom_components/powercalc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
ENERGY_INTEGRATION_METHODS,
ENTITY_CATEGORIES,
MIN_HA_VERSION,
SERVICE_CHANGE_GUI_CONFIGURATION,
PowercalcDiscoveryType,
SensorType,
UnitPrefix,
Expand All @@ -82,6 +83,7 @@
remove_group_from_power_sensor_entry,
remove_power_sensor_from_associated_groups,
)
from .service.gui_configuration import SERVICE_SCHEMA, change_gui_configuration
from .strategy.factory import PowerCalculatorStrategyFactory

PLATFORMS = [Platform.SENSOR]
Expand Down Expand Up @@ -217,42 +219,32 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
DATA_STANDBY_POWER_SENSORS: {},
}

await hass.async_add_executor_job(register_services, hass)

if domain_config.get(CONF_ENABLE_AUTODISCOVERY):
discovery_manager = DiscoveryManager(hass, config)
await discovery_manager.start_discovery()

sensors: list = domain_config.get(CONF_SENSORS, [])
sorted_sensors = sorted(
sensors,
key=lambda item: 1 if CONF_INCLUDE in item else 0,
)
for sensor_config in sorted_sensors:
sensor_config.update({DISCOVERY_TYPE: PowercalcDiscoveryType.USER_YAML})
hass.async_create_task(
async_load_platform(
hass,
Platform.SENSOR,
DOMAIN,
sensor_config,
config,
),
)
await setup_yaml_sensors(hass, config, domain_config)

domain_groups: list[str] | None = domain_config.get(CONF_CREATE_DOMAIN_GROUPS)
if domain_groups:
setup_domain_groups(hass, domain_config)
setup_standby_group(hass, domain_config)

async def _create_domain_groups(event: None) -> None:
await create_domain_groups(
hass,
domain_config,
domain_groups,
)
return True


def register_services(hass: HomeAssistant) -> None:
"""Register generic services"""

hass.services.register(
DOMAIN,
SERVICE_CHANGE_GUI_CONFIGURATION,
lambda call: change_gui_configuration(hass, call),
schema=SERVICE_SCHEMA,
)

hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED,
_create_domain_groups,
)

def setup_standby_group(hass: HomeAssistant, domain_config: ConfigType) -> None:
async def _create_standby_group(event: None) -> None:
hass.async_create_task(
async_load_platform(
Expand All @@ -269,7 +261,59 @@ async def _create_standby_group(event: None) -> None:
_create_standby_group,
)

return True

def setup_domain_groups(hass: HomeAssistant, global_config: ConfigType) -> None:
domain_groups: list[str] | None = global_config.get(CONF_CREATE_DOMAIN_GROUPS)
if not domain_groups:
return

async def _create_domain_groups(event: None) -> None:
"""Create group sensors aggregating all power sensors from given domains."""
_LOGGER.debug("Setting up domain based group sensors..")
for domain in domain_groups:
if domain not in hass.data[DOMAIN].get(DATA_DOMAIN_ENTITIES):
_LOGGER.error("Cannot setup group for domain %s, no entities found", domain)
continue

domain_entities = hass.data[DOMAIN].get(DATA_DOMAIN_ENTITIES)[domain]

hass.async_create_task(
async_load_platform(
hass,
SENSOR_DOMAIN,
DOMAIN,
{
DISCOVERY_TYPE: PowercalcDiscoveryType.DOMAIN_GROUP,
CONF_ENTITIES: domain_entities,
CONF_DOMAIN: domain,
},
global_config,
),
)

hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED,
_create_domain_groups,
)


async def setup_yaml_sensors(hass: HomeAssistant, config: ConfigType, domain_config: ConfigType) -> None:
sensors: list = domain_config.get(CONF_SENSORS, [])
sorted_sensors = sorted(
sensors,
key=lambda item: 1 if CONF_INCLUDE in item else 0,
)
for sensor_config in sorted_sensors:
sensor_config.update({DISCOVERY_TYPE: PowercalcDiscoveryType.USER_YAML})
hass.async_create_task(
async_load_platform(
hass,
Platform.SENSOR,
DOMAIN,
sensor_config,
config,
),
)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Expand Down Expand Up @@ -337,35 +381,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return True


async def create_domain_groups(
hass: HomeAssistant,
global_config: ConfigType,
domains: list[str],
) -> None:
"""Create group sensors aggregating all power sensors from given domains."""
_LOGGER.debug("Setting up domain based group sensors..")
for domain in domains:
if domain not in hass.data[DOMAIN].get(DATA_DOMAIN_ENTITIES):
_LOGGER.error("Cannot setup group for domain %s, no entities found", domain)
continue

domain_entities = hass.data[DOMAIN].get(DATA_DOMAIN_ENTITIES)[domain]

hass.async_create_task(
async_load_platform(
hass,
SENSOR_DOMAIN,
DOMAIN,
{
DISCOVERY_TYPE: PowercalcDiscoveryType.DOMAIN_GROUP,
CONF_ENTITIES: domain_entities,
CONF_DOMAIN: domain,
},
global_config,
),
)


def _notify_message(
hass: HomeAssistant,
notification_id: str,
Expand Down
18 changes: 15 additions & 3 deletions custom_components/powercalc/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
DISCOVERY_POWER_PROFILE,
DISCOVERY_SOURCE_ENTITY,
DOMAIN,
DOMAIN_CONFIG,
DUMMY_ENTITY_ID,
ENERGY_INTEGRATION_METHOD_LEFT,
ENERGY_INTEGRATION_METHODS,
Expand Down Expand Up @@ -704,7 +705,7 @@ async def async_step_power_advanced(

return self.async_show_form(
step_id="power_advanced",
data_schema=SCHEMA_POWER_ADVANCED,
data_schema=_fill_schema_defaults(SCHEMA_POWER_ADVANCED, _get_global_powercalc_config(self.hass)),
errors={},
)

Expand Down Expand Up @@ -966,9 +967,15 @@ def _create_virtual_power_schema(
): STRATEGY_SELECTOR,
},
)
return schema.extend(SCHEMA_POWER_OPTIONS.schema) # type: ignore
options_schema = SCHEMA_POWER_OPTIONS
else:
options_schema = SCHEMA_POWER_OPTIONS_LIBRARY

return schema.extend(SCHEMA_POWER_OPTIONS_LIBRARY.schema) # type: ignore
power_options = _fill_schema_defaults(
options_schema,
_get_global_powercalc_config(hass),
)
return schema.extend(power_options.schema) # type: ignore


def _create_group_options_schema(
Expand Down Expand Up @@ -1210,3 +1217,8 @@ def _fill_schema_defaults(
new_key.description = {"suggested_value": options.get(key)} # type: ignore
schema[new_key] = val
return vol.Schema(schema)


def _get_global_powercalc_config(hass: HomeAssistant) -> dict[str, str]:
powercalc = hass.data.get(DOMAIN) or {}
return powercalc.get(DOMAIN_CONFIG) or {}
1 change: 1 addition & 0 deletions custom_components/powercalc/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ class UnitPrefix(StrEnum):
SERVICE_CALIBRATE_UTILITY_METER = "calibrate_utility_meter"
SERVICE_CALIBRATE_ENERGY = "calibrate_energy"
SERVICE_SWITCH_SUB_PROFILE = "switch_sub_profile"
SERVICE_CHANGE_GUI_CONFIGURATION = "change_gui_config"

SIGNAL_POWER_SENSOR_STATE_CHANGE = "powercalc_power_sensor_state_change"

Expand Down
Empty file.
45 changes: 45 additions & 0 deletions custom_components/powercalc/service/gui_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass

from custom_components.powercalc import (
CONF_CREATE_UTILITY_METERS,
CONF_ENERGY_INTEGRATION_METHOD,
CONF_IGNORE_UNAVAILABLE_STATE,
DOMAIN,
ENERGY_INTEGRATION_METHODS,
)
from custom_components.powercalc.const import CONF_CREATE_ENERGY_SENSOR

ALLOWED_CONFIG_KEYS = [
CONF_CREATE_ENERGY_SENSOR,
CONF_CREATE_UTILITY_METERS,
CONF_IGNORE_UNAVAILABLE_STATE,
CONF_ENERGY_INTEGRATION_METHOD,
]

SERVICE_SCHEMA = vol.Schema({
vol.Required("field"): vol.In(ALLOWED_CONFIG_KEYS),
vol.Required("value"): cv.string,
})


@bind_hass
def change_gui_configuration(hass: HomeAssistant, call: ServiceCall) -> None:
field = call.data["field"]
value = call.data["value"]

if field in [CONF_CREATE_ENERGY_SENSOR, CONF_CREATE_UTILITY_METERS, CONF_IGNORE_UNAVAILABLE_STATE]:
value = cv.boolean(value)

if field == CONF_ENERGY_INTEGRATION_METHOD and value not in ENERGY_INTEGRATION_METHODS:
raise HomeAssistantError(f"Invalid integration method {value}")

for entry in hass.config_entries.async_entries(DOMAIN):
if field not in entry.data:
continue
new_data = entry.data.copy()
new_data[field] = value
hass.config_entries.async_update_entry(entry, data=new_data)
20 changes: 20 additions & 0 deletions custom_components/powercalc/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,25 @@ switch_sub_profile:
description: Define one of the possible sub profiles
example: "nigh_vision"
required: true
selector:
text:
change_gui_config:
name: Change GUI config
description: Batch change configuration of all Powercalc config entries
fields:
field:
name: Field
required: true
selector:
select:
mode: dropdown
options:
- "create_energy_sensor"
- "create_utility_meters"
- "ignore_unavailable_state"
- "energy_integration_method"
value:
name: Value
required: true
selector:
text:
24 changes: 24 additions & 0 deletions docs/source/configuration/global-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Global configuration
Powercalc provides some configuration which can be applied on a global level. This means any of this configuration option applies to all sensors created with powercalc.
Any configuration you do on a per sensor basis will override the global setting for that sensor.

.. note::
Sensors created with the GUI do have a configuration set for ``create_energy_sensors``, ``create_utility_meters``, ``ignore_unavailable_state`` and ``energy_integration_method``, changing global configuration will not affect the existing GUI configuration entries, to make it easy to change all of them Powercalc provides a service ``powercalc.change_gui_config``. Refer to `Change GUI configuration service`_.

You can add these options to `configuration.yaml` under the ``powercalc:`` property, like so:

.. code-block:: yaml
Expand Down Expand Up @@ -55,3 +58,24 @@ All the possible options are listed below.
+-------------------------------+----------+--------------+-------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| utility_meter_tariffs | list | **Optional** | | Define different tariffs. See `HA docs <https://www.home-assistant.io/integrations/utility_meter/#tariffs>`_. |
+-------------------------------+----------+--------------+-------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

Change GUI configuration service
--------------------------------

To change the configuration options for all Powercalc GUI config entries at once you can utilize the service ``powercalc.change_gui_config``.
You can use it to change the configuration for the following options

- create_energy_sensor
- create_utility_meters
- ignore_unavailable_state
- energy_integration_method

You can call this service from the GUI (:guilabel:`Developer tools` -> :guilabel:`Services`).
For example to set ``create_utility_meters`` to yes for all powercalc GUI configurations:

.. code-block:: yaml
service: powercalc.change_gui_config
data:
field: create_utility_meters
value: 1
39 changes: 39 additions & 0 deletions tests/config_flow/test_virtual_power_fixed.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
from unittest.mock import patch

import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import HomeAssistant

from custom_components.powercalc import CONF_FIXED, CONF_POWER_TEMPLATE
from custom_components.powercalc.const import (
CONF_CALCULATION_ENABLED_CONDITION,
CONF_CREATE_UTILITY_METERS,
CONF_ENERGY_INTEGRATION_METHOD,
CONF_IGNORE_UNAVAILABLE_STATE,
CONF_MODE,
CONF_POWER,
CONF_SENSOR_TYPE,
CONF_STATES_POWER,
ENERGY_INTEGRATION_METHOD_RIGHT,
CalculationStrategy,
SensorType,
)
from custom_components.powercalc.errors import StrategyConfigurationError
from tests.common import run_powercalc_setup
from tests.config_flow.common import (
assert_default_virtual_power_entry_data,
create_mock_entry,
Expand Down Expand Up @@ -161,3 +167,36 @@ async def test_entity_selection_mandatory(hass: HomeAssistant) -> None:
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["errors"] == {"entity_id": "entity_mandatory"}


async def test_global_configuration_is_applied_to_field_default(hass: HomeAssistant) -> None:
"""Field should be set to match powercalc global configuration by default"""
global_config = {
CONF_CREATE_UTILITY_METERS: True,
CONF_ENERGY_INTEGRATION_METHOD: ENERGY_INTEGRATION_METHOD_RIGHT,
CONF_IGNORE_UNAVAILABLE_STATE: True,
}
await run_powercalc_setup(hass, {}, global_config)

result = await select_sensor_type(hass, SensorType.VIRTUAL_POWER)
assert result["type"] == data_entry_flow.FlowResultType.FORM
schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys())
assert schema_keys[schema_keys.index(CONF_CREATE_UTILITY_METERS)].description == {"suggested_value": True}

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_ENTITY_ID: "light.test",
CONF_MODE: CalculationStrategy.FIXED,
},
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_POWER: 50},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "power_advanced"
schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys())
assert schema_keys[schema_keys.index(CONF_ENERGY_INTEGRATION_METHOD)].default() == ENERGY_INTEGRATION_METHOD_RIGHT
assert schema_keys[schema_keys.index(CONF_IGNORE_UNAVAILABLE_STATE)].description == {"suggested_value": True}

Empty file added tests/service/__init__.py
Empty file.
Loading

0 comments on commit 3584079

Please sign in to comment.