-
-
Notifications
You must be signed in to change notification settings - Fork 37.2k
Add Powersensor integration #158505
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
base: dev
Are you sure you want to change the base?
Add Powersensor integration #158505
Changes from all commits
2e26b76
9a4b0f6
51c798d
83f9ca3
5d4f5f1
104fe51
e625e14
7009f04
64f39b2
a05b135
c118e5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| """The Powersensor integration.""" | ||
|
|
||
| import logging | ||
|
|
||
| from powersensor_local import VirtualHousehold | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import Platform | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.exceptions import ConfigEntryNotReady | ||
| from homeassistant.loader import async_get_integration | ||
|
|
||
| from .config_flow import PowersensorConfigFlow | ||
| from .const import ( | ||
| CFG_DEVICES, | ||
| CFG_ROLES, | ||
| DOMAIN, | ||
| ROLE_SOLAR, | ||
| RT_DISPATCHER, | ||
| RT_VHH, | ||
| RT_ZEROCONF, | ||
| ) | ||
| from .powersensor_discovery_service import PowersensorDiscoveryService | ||
| from .powersensor_message_dispatcher import PowersensorMessageDispatcher | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| PLATFORMS: list[Platform] = [Platform.SENSOR] | ||
|
|
||
| # | ||
| # config entry.data structure (version 2.2): | ||
| # { | ||
| # devices = { | ||
| # mac = { | ||
| # name =, | ||
| # display_name =, | ||
| # mac =, | ||
| # host =, | ||
| # port =, | ||
| # } | ||
| # roles = { | ||
| # mac = role, | ||
| # } | ||
| # } | ||
| # | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Set up integration from a config entry.""" | ||
|
|
||
| hass.data.setdefault(DOMAIN, {}) | ||
| hass.data[DOMAIN][entry.entry_id] = {} | ||
|
|
||
| integration = await async_get_integration(hass, DOMAIN) | ||
| manifest = integration.manifest | ||
|
|
||
| try: | ||
| # Create the zeroconf discovery service | ||
| zeroconf_domain: str = str(manifest["zeroconf"][0]) | ||
| zeroconf_service = PowersensorDiscoveryService(hass, zeroconf_domain) | ||
| await zeroconf_service.start() | ||
|
|
||
| # Establish our virtual household | ||
| with_solar = ROLE_SOLAR in entry.data.get(CFG_ROLES, {}).values() | ||
| vhh = VirtualHousehold(with_solar) | ||
|
|
||
| # Set up message dispatcher | ||
| dispatcher = PowersensorMessageDispatcher(hass, entry, vhh) | ||
| for network_info in entry.data.get(CFG_DEVICES, {}).values(): | ||
| await dispatcher.enqueue_plug_for_adding(network_info) | ||
| except Exception as err: | ||
| raise ConfigEntryNotReady(f"Unexpected error during setup: {err}") from err | ||
|
|
||
| entry.runtime_data = { | ||
| RT_VHH: vhh, | ||
| RT_DISPATCHER: dispatcher, | ||
| RT_ZEROCONF: zeroconf_service, | ||
| } | ||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Unload a config entry.""" | ||
| _LOGGER.debug("Started unloading for %s", entry.entry_id) | ||
| if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
| if hasattr(entry, "runtime_data"): | ||
| if RT_DISPATCHER in entry.runtime_data: | ||
| await entry.runtime_data[RT_DISPATCHER].disconnect() | ||
| if RT_ZEROCONF in entry.runtime_data: | ||
| await entry.runtime_data[RT_ZEROCONF].stop() | ||
|
|
||
| if entry.entry_id in hass.data[DOMAIN]: | ||
| hass.data[DOMAIN].pop(entry.entry_id) | ||
|
|
||
| return unload_ok | ||
|
|
||
|
|
||
| async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Migrate old config entry.""" | ||
| _LOGGER.debug("Upgrading config from %s.%s", entry.version, entry.minor_version) | ||
| if entry.version > PowersensorConfigFlow.VERSION: | ||
| # Downgrade from future version | ||
| return False | ||
|
|
||
| if entry.version == 1: | ||
| # Move device info into subkey | ||
| devices = {**entry.data} | ||
| new_data = {CFG_DEVICES: devices, CFG_ROLES: {}} | ||
| hass.config_entries.async_update_entry( | ||
| entry, data=new_data, version=2, minor_version=2 | ||
| ) | ||
|
|
||
|
bookman-dius marked this conversation as resolved.
|
||
| _LOGGER.debug("Upgrading config to %s.%s", entry.version, entry.minor_version) | ||
| return True | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,212 @@ | ||||||||
| """Config flow for the integration.""" | ||||||||
|
|
||||||||
| import logging | ||||||||
| from typing import Any | ||||||||
|
|
||||||||
| import voluptuous as vol | ||||||||
|
|
||||||||
| from homeassistant import config_entries | ||||||||
| from homeassistant.core import HomeAssistant | ||||||||
| from homeassistant.config_entries import ConfigEntry, ConfigFlowResult | ||||||||
| from homeassistant.helpers.dispatcher import async_dispatcher_send | ||||||||
| from homeassistant.helpers.selector import selector | ||||||||
| from homeassistant.helpers.service_info import zeroconf | ||||||||
| from homeassistant.helpers.translation import async_get_cached_translations | ||||||||
|
|
||||||||
| from .const import ( | ||||||||
| CFG_DEVICES, | ||||||||
| CFG_ROLES, | ||||||||
| DEFAULT_PORT, | ||||||||
| DOMAIN, | ||||||||
| ROLE_APPLIANCE, | ||||||||
| ROLE_HOUSENET, | ||||||||
| ROLE_SOLAR, | ||||||||
| ROLE_UPDATE_SIGNAL, | ||||||||
| ROLE_WATER, | ||||||||
| RT_DISPATCHER, | ||||||||
| ) | ||||||||
|
|
||||||||
| _LOGGER = logging.getLogger(__name__) | ||||||||
|
|
||||||||
|
|
||||||||
| def get_translated_sensor_name(hass: HomeAssistant, config_entry: ConfigEntry, mac: str) -> str|None: | ||||||||
| """Helper to neatly format the translated name, for user input.""" | ||||||||
| translations = async_get_cached_translations( | ||||||||
| hass, hass.config.language, "device", config_entry.domain | ||||||||
| ) | ||||||||
| format_string = translations.get( | ||||||||
| "component.powersensor.device.unknown_sensor.name", | ||||||||
| "Powersensor Sensor (ID: {id})" | ||||||||
| ) | ||||||||
| return format_string.replace("{id}", mac) | ||||||||
|
|
||||||||
|
|
||||||||
| class PowersensorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||||||||
| """Handle a config flow.""" | ||||||||
|
|
||||||||
| VERSION = 2 | ||||||||
|
||||||||
| VERSION = 2 | |
| VERSION = 2 | |
| MINOR_VERSION = 2 |
Copilot
AI
Feb 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty __init__ method that provides no functionality. The __init__ method on lines 37-38 is empty and serves no purpose. It can be removed entirely as Python will use the default constructor from the parent class.
| def __init__(self) -> None: | |
| """Initialize the config flow.""" |
Copilot
AI
Feb 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Config flow abort reasons are currently full sentences (e.g. "Cannot reconfigure..." and "Plug firmware not compatible"). Abort reason values are expected to be stable translation keys (backed by strings.json), otherwise users will see raw strings and translations cannot be provided. Replace these with short keys (e.g. cannot_reconfigure, incompatible_firmware) and add corresponding translations.
Copilot
AI
Feb 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_common_setup() can return an abort FlowResult, but async_step_zeroconf ignores the return value and continues. Return early when _common_setup() aborts to avoid continuing the flow when an entry already exists / another flow is in progress.
Copilot
AI
Feb 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This debug log call passes a dict as the message (_LOGGER.debug(self.hass.data[DOMAIN]["discovered_plugs"])) which bypasses lazy formatting and produces an unstructured message. Use a descriptive format string with %s so the message is clear and avoids unnecessary stringification when debug logging is disabled.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| """Constants for the Powersensor integration.""" | ||
|
|
||
| DOMAIN = "powersensor" | ||
| DEFAULT_NAME = "Powersensor" | ||
| DEFAULT_PORT = 49476 | ||
|
|
||
| # Internal signals | ||
| CREATE_PLUG_SIGNAL = f"{DOMAIN}_create_plug" | ||
| CREATE_SENSOR_SIGNAL = f"{DOMAIN}_create_sensor" | ||
| DATA_UPDATE_SIGNAL_FMT_MAC_EVENT = f"{DOMAIN}_data_update_%s_%s" | ||
| ROLE_UPDATE_SIGNAL = f"{DOMAIN}_update_role" | ||
| PLUG_ADDED_TO_HA_SIGNAL = f"{DOMAIN}_plug_added_to_homeassistant" | ||
| SENSOR_ADDED_TO_HA_SIGNAL = f"{DOMAIN}_sensor_added_to_homeassistant" | ||
| UPDATE_VHH_SIGNAL = f"{DOMAIN}_update_vhh" | ||
| ZEROCONF_ADD_PLUG_SIGNAL = f"{DOMAIN}_zeroconf_add_plug" | ||
| ZEROCONF_REMOVE_PLUG_SIGNAL = f"{DOMAIN}_zeroconf_remove_plug" | ||
| ZEROCONF_UPDATE_PLUG_SIGNAL = f"{DOMAIN}_zeroconf_update_plug" | ||
|
|
||
| # Config entry keys | ||
| CFG_DEVICES = "devices" | ||
| CFG_ROLES = "roles" | ||
|
|
||
| # Role names (fixed, as-received from plug API) | ||
| ROLE_APPLIANCE = "appliance" | ||
| ROLE_HOUSENET = "house-net" | ||
| ROLE_SOLAR = "solar" | ||
| ROLE_WATER = "water" | ||
|
|
||
| # runtime_data keys | ||
| RT_DISPATCHER = "dispatcher" | ||
| RT_VHH = "vhh" | ||
| RT_VHH_LOCK = "vhh_update_lock" | ||
| RT_VHH_MAINS_ADDED = "vhh_main_added" | ||
| RT_VHH_SOLAR_ADDED = "vhh_solar_added" | ||
| RT_ZEROCONF = "zeroconf" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| { | ||
| "domain": "powersensor", | ||
| "name": "Powersensor", | ||
| "codeowners": [ | ||
| "@bookman-dius", | ||
| "@jmattsson" | ||
| ], | ||
| "config_flow": true, | ||
| "dependencies": [], | ||
| "documentation": "https://www.home-assistant.io/integrations/powersensor", | ||
| "integration_type": "hub", | ||
| "iot_class": "local_push", | ||
| "quality_scale": "bronze", | ||
| "requirements": ["powersensor-local==2.1.1"], | ||
| "single_config_entry": true, | ||
| "zeroconf": ["_powersensor._udp.local."] | ||
| } | ||
|
bookman-dius marked this conversation as resolved.
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async_setup_entrywraps the whole setup inexcept Exceptionand converts it toConfigEntryNotReady. Catching all exceptions here can mask programming errors and cause the entry to retry forever. Prefer catching specific expected exceptions from zeroconf/PlugApi startup (e.g., timeouts/OSError) and let unexpected exceptions bubble up.