Skip to content

Commit

Permalink
Implement new discovery through GUI (#1115)
Browse files Browse the repository at this point in the history
  • Loading branch information
bramstroker committed Sep 23, 2022
1 parent 8ca10cc commit 7801795
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 118 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,22 @@ powercalc:

### Setup power sensors

Powercalc has a build-in library of more than 190 light models ([LUT](#lut-mode)), which have been measured and provided by users. See [supported models](docs/supported_models.md).
Powercalc has a build-in library of more than 190 power profiles. Currently, this exists mostly of lights.
These profiles have been measured and provided by users. See [supported models](docs/supported_models.md) for the listing of supported devices.

Starting from 0.12.0 Powercalc can automatically discover entities in your HA instance which are supported for automatic configuration.
After intallation and restarting HA power and energy sensors should appear. When this is not the case please check the logs for any errors.
Powercalc scans your HA instance for entities which are supported for automatic configuration. It does that based on the manufacturer and model information known in HA.
After following the installation steps above and restarting HA power and energy sensors should appear.
When this is not the case please check the logs for any errors, you can also enable debug logging to get more details about the discovery routine.

When your appliance is not supported you have extensive options for manual configuration. These are explained below.
When your appliance is not supported out of the box (or you want to have more control) you have extensive options for manual configuration. These are explained below.

> Note: Manually configuring an entity will override an auto discovered entity
## Configuration

To manually add virtual sensors for your devices you have to add some configuration to `configuration.yaml`, or you can use the GUI configuration ("Settings" -> "Devices & Services" -> "Add integration" -> "Powercalc") and follow the instructions.

Additionally some settings can be applied on global level and will apply to all your virtual power sensors. This global configuration cannot be configured using the GUI yet.
Additionally, some settings can be applied on global level and will apply to all your virtual power sensors. This global configuration cannot be configured using the GUI yet.

After changing the configuration you need to restart HA to get your power sensors to appear. This is only necessary for changes in the YAML files.

Expand Down
262 changes: 170 additions & 92 deletions custom_components/powercalc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,29 @@
import homeassistant.helpers.entity_registry as er
import voluptuous as vol
from awesomeversion.awesomeversion import AwesomeVersion
from homeassistant.helpers.typing import ConfigType
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.utility_meter import DEFAULT_OFFSET, max_28_days
from homeassistant.components.utility_meter.const import METER_TYPES
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, SOURCE_INTEGRATION_DISCOVERY, SOURCE_USER
from homeassistant.const import (
CONF_DOMAIN,
CONF_ENTITIES,
CONF_ENTITY_ID,
CONF_NAME,
CONF_UNIQUE_ID,
CONF_PLATFORM,
CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_STARTED,
Platform,
)
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import discovery, discovery_flow

from .common import create_source_entity, validate_name_pattern
from .common import create_source_entity, validate_name_pattern, SourceEntity
from .const import (
CONF_CREATE_DOMAIN_GROUPS,
CONF_CREATE_ENERGY_SENSORS,
Expand All @@ -40,6 +43,8 @@
CONF_ENERGY_SENSOR_PRECISION,
CONF_ENERGY_SENSOR_UNIT_PREFIX,
CONF_FORCE_UPDATE_FREQUENCY,
CONF_MANUFACTURER,
CONF_MODEL,
CONF_POWER_SENSOR_CATEGORY,
CONF_POWER_SENSOR_FRIENDLY_NAMING,
CONF_POWER_SENSOR_NAMING,
Expand Down Expand Up @@ -75,6 +80,7 @@
from .power_profile.model_discovery import (
get_power_profile,
has_manufacturer_and_model_information,
PowerProfile,
)
from .sensors.group import update_associated_group_entry
from .strategy.factory import PowerCalculatorStrategyFactory
Expand Down Expand Up @@ -185,7 +191,9 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
DATA_USED_UNIQUE_IDS: [],
}

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

if domain_config.get(CONF_CREATE_DOMAIN_GROUPS):

Expand Down Expand Up @@ -239,93 +247,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return unload_ok


async def autodiscover_entities(config: dict, domain_config: dict, hass: HomeAssistant):
"""Discover entities supported for powercalc autoconfiguration in HA instance"""

if not domain_config.get(CONF_ENABLE_AUTODISCOVERY):
return

_LOGGER.debug("Start auto discovering entities")
entity_registry = er.async_get(hass)
for entity_entry in list(entity_registry.entities.values()):
if entity_entry.disabled:
continue

if entity_entry.domain not in (LIGHT_DOMAIN, SWITCH_DOMAIN):
continue

if not await has_manufacturer_and_model_information(hass, entity_entry):
continue

source_entity = await create_source_entity(entity_entry.entity_id, hass)
try:
power_profile = await get_power_profile(
hass, {}, source_entity.entity_entry
)
if not power_profile:
continue
except ModelNotSupported:
_LOGGER.debug(
"%s: Model not found in library, skipping auto configuration",
entity_entry.entity_id,
)
continue

has_user_config = is_user_configured(hass, config, entity_entry.entity_id)

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

if has_user_config:
_LOGGER.debug(
"%s: Entity is manually configured, skipping auto configuration",
entity_entry.entity_id,
)
continue

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

discovery_info = {
CONF_ENTITY_ID: entity_entry.entity_id,
DISCOVERY_SOURCE_ENTITY: source_entity,
DISCOVERY_POWER_PROFILE: power_profile,
DISCOVERY_TYPE: PowercalcDiscoveryType.LIBRARY,
}
hass.async_create_task(
discovery.async_load_platform(
hass, SENSOR_DOMAIN, DOMAIN, discovery_info, config
)
)

_LOGGER.debug("Done auto discovering entities")


def is_user_configured(hass: HomeAssistant, config: dict, entity_id: str) -> bool:
"""
Check if user have setup powercalc sensors for a given entity_id.
Either with the YAML or GUI method.
"""
if SENSOR_DOMAIN in config:
sensor_config = config.get(SENSOR_DOMAIN)
for item in sensor_config:
if (
isinstance(item, dict)
and item.get(CONF_PLATFORM) == DOMAIN
and item.get(CONF_ENTITY_ID) == entity_id
):
return True

for config_entry in hass.config_entries.async_entries(DOMAIN):
if config_entry.data.get(CONF_ENTITY_ID) == entity_id:
return True

return False


async def create_domain_groups(
hass: HomeAssistant, global_config: dict, domains: list[str]
):
Expand All @@ -351,3 +272,160 @@ async def create_domain_groups(
global_config,
)
)


class DiscoveryManager:
def __init__(self, hass: HomeAssistant, ha_config: ConfigType):
self.hass = hass
self.ha_config = ha_config
self.manually_configured_entities: list[str] | None = None

async def start_discovery(self):
"""Discover entities supported for powercalc autoconfiguration in HA instance"""

_LOGGER.debug("Start auto discovering entities")
entity_registry = er.async_get(self.hass)
for entity_entry in list(entity_registry.entities.values()):
if entity_entry.disabled:
continue

if entity_entry.domain not in (LIGHT_DOMAIN, SWITCH_DOMAIN):
continue

if not await has_manufacturer_and_model_information(self.hass, entity_entry):
continue

source_entity = await create_source_entity(entity_entry.entity_id, self.hass)
try:
power_profile = await get_power_profile(
self.hass, {}, source_entity.entity_entry
)
if not power_profile:
continue
except ModelNotSupported:
_LOGGER.debug(
"%s: Model not found in library, skipping discovery",
entity_entry.entity_id,
)
continue

has_user_config = self._is_user_configured(entity_entry.entity_id)

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

if has_user_config:
_LOGGER.debug(
"%s: Entity is manually configured, skipping auto configuration",
entity_entry.entity_id,
)
continue

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

self._init_entity_discovery(source_entity, power_profile)

_LOGGER.debug("Done auto discovering entities")

@callback
def _init_entity_discovery(self, source_entity: SourceEntity, power_profile: PowerProfile):
existing_entries = [
entry
for entry in
self.hass.config_entries.async_entries(DOMAIN)
if entry.unique_id == source_entity.unique_id
]
if existing_entries:
_LOGGER.debug(f"{source_entity.entity_id}: Already setup with discovery, skipping new discovery")
return

discovery_flow.async_create_flow(
self.hass,
DOMAIN,
context={"source": SOURCE_INTEGRATION_DISCOVERY},
data={
CONF_UNIQUE_ID: source_entity.unique_id,
CONF_NAME: source_entity.name,
CONF_ENTITY_ID: source_entity.entity_id,
CONF_MANUFACTURER: power_profile.manufacturer,
CONF_MODEL: power_profile.model,
}
)

# Code below if for legacy discovery routine, will be removed somewhere in the future
discovery_info = {
CONF_ENTITY_ID: source_entity.entity_id,
DISCOVERY_SOURCE_ENTITY: source_entity,
DISCOVERY_POWER_PROFILE: power_profile,
DISCOVERY_TYPE: PowercalcDiscoveryType.LIBRARY,
}
self.hass.async_create_task(
discovery.async_load_platform(
self.hass, SENSOR_DOMAIN, DOMAIN, discovery_info, self.ha_config
)
)

def _is_user_configured(self, entity_id: str) -> bool:
"""
Check if user have setup powercalc sensors for a given entity_id.
Either with the YAML or GUI method.
"""
if not self.manually_configured_entities:
self.manually_configured_entities = self._load_manually_configured_entities()

return entity_id in self.manually_configured_entities

def _load_manually_configured_entities(self) -> list[str]:
entities = []

# Find entity ids in yaml config
if SENSOR_DOMAIN in self.ha_config:
sensor_config = self.ha_config.get(SENSOR_DOMAIN)
platform_entries = [
item for item in sensor_config
if isinstance(item, dict) and item.get(CONF_PLATFORM) == DOMAIN
]
for entry in platform_entries:
entities.extend(self._find_entity_ids_in_yaml_config(entry))

# Add entities from existing config entries
entities.extend(
[
entry.data.get(CONF_ENTITY_ID)
for entry in self.hass.config_entries.async_entries(DOMAIN)
if entry.source == SOURCE_USER
]
)

return entities

def _find_entity_ids_in_yaml_config(self, search_dict: dict):
"""
Takes a dict with nested lists and dicts,
and searches all dicts for a key of the field
provided.
"""
found_entity_ids = []

for key, value in search_dict.items():

if key == "entity_id":
found_entity_ids.append(value)

elif isinstance(value, dict):
results = self._find_entity_ids_in_yaml_config(value)
for result in results:
found_entity_ids.append(result)

elif isinstance(value, list):
for item in value:
if isinstance(item, dict):
results = self._find_entity_ids_in_yaml_config(item)
for result in results:
found_entity_ids.append(result)

return found_entity_ids
33 changes: 33 additions & 0 deletions custom_components/powercalc/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
from homeassistant.helpers.typing import DiscoveryInfoType

from .common import SourceEntity, create_source_entity
from .const import (
Expand Down Expand Up @@ -190,6 +191,38 @@ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)

async def async_step_integration_discovery(
self, discovery_info: DiscoveryInfoType
) -> FlowResult:
"""Handle integration discovery."""

self.sensor_config.update(discovery_info)
self.sensor_config.update({CONF_MODE: CalculationStrategy.LUT})
self.selected_sensor_type = SensorType.VIRTUAL_POWER
self.name = discovery_info[CONF_NAME]
unique_id = discovery_info[CONF_UNIQUE_ID]
await self.async_set_unique_id(unique_id)

return await self.async_step_discovery_confirm()

async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm discovery."""

if user_input is not None:
return self.create_config_entry()
self._set_confirm_only()
placeholders = {
"name": self.sensor_config.get(CONF_NAME),
"manufacturer": self.sensor_config.get(CONF_MANUFACTURER),
"model": self.sensor_config.get(CONF_MODEL),
}
self.context["title_placeholders"] = placeholders
return self.async_show_form(
step_id="discovery_confirm", description_placeholders=placeholders
)

async def async_step_user(self, user_input=None) -> FlowResult:
"""Handle the initial step."""

Expand Down
Loading

0 comments on commit 7801795

Please sign in to comment.