Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

0.1.0 Release #57

Merged
merged 51 commits into from
Dec 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a3ab09e
create beta version; update pyduke-energy ref
mjmeli Oct 25, 2021
69e2026
use the new select_default_meter client lib method to select meter
mjmeli Oct 25, 2021
ea5970b
Merge pull request #27 from mjmeli/use-select-default-meter
mjmeli Oct 25, 2021
0fcacb9
update pyduke-energy version to 0.0.9
mjmeli Oct 26, 2021
d8f4f1e
initial setup of realtime stream into the integration
mjmeli Oct 26, 2021
01f23da
update exception messages
mjmeli Oct 26, 2021
c4ff572
hook up dispatch on receive of a realtime measurement
mjmeli Oct 28, 2021
7a450ed
new entity for current power usage; update device identifiers
mjmeli Oct 29, 2021
cc9833e
handle migration of old device to new device identifiers
mjmeli Oct 29, 2021
c54ab6e
refactoring of how sensors are created
mjmeli Oct 29, 2021
14264f6
big refactor of how sensors are defined. should be in a good state now
mjmeli Oct 29, 2021
429b274
linting fixes
mjmeli Oct 29, 2021
81e6769
hook up the realtime sensor to dispatcher
mjmeli Oct 29, 2021
2daef38
pull realtime energy usage into HA
mjmeli Oct 29, 2021
0fe9d93
make sure the MQTT stream is unubsubscribed when reloading/removing i…
mjmeli Oct 29, 2021
ca33349
hook up real-time update interval value
mjmeli Oct 29, 2021
abe285d
promote default state property func to base sensor
mjmeli Oct 29, 2021
1f27438
add poll boolean to sensor metadata and use update method
mjmeli Oct 29, 2021
9b73d98
fix linting
mjmeli Oct 29, 2021
9b208c6
add real-time data throttling to avoid excessive data updates
mjmeli Oct 29, 2021
98b55c1
tick pyduke-energy version v0.0.10
mjmeli Oct 29, 2021
fadec85
validate cleanup of subscriptions on teardown
mjmeli Oct 29, 2021
f59eb88
minor comment/log wording changes
mjmeli Oct 29, 2021
9e28a45
Merge pull request #28 from mjmeli/realtime
mjmeli Oct 29, 2021
32943f4
tick version v0.1.0b1
mjmeli Oct 29, 2021
0be7d15
fix migration error
mjmeli Oct 30, 2021
a40d75c
Merge pull request #29 from mjmeli/fix-migration-error
mjmeli Oct 30, 2021
6011a7c
remove throttling by default
mjmeli Oct 30, 2021
ca09db3
update readme with new sensor info
mjmeli Oct 30, 2021
c5860c4
Merge pull request #30 from mjmeli/realtime-updates
mjmeli Oct 30, 2021
cbd56b1
tick version v0.1.0b3
mjmeli Oct 30, 2021
0d7a5ba
add rounding to usage today sensor
mjmeli Oct 30, 2021
6596e6d
Merge pull request #31 from mjmeli/rounding
mjmeli Oct 30, 2021
fa67094
update readme for real-time upd
mjmeli Nov 1, 2021
50d759e
fix linting issues, tick pyduke-energy version
mjmeli Nov 1, 2021
4a2c8ed
Merge pull request #33 from mjmeli/rounding
mjmeli Nov 1, 2021
612149d
bump pyduke-energy to v0.0.12
mjmeli Nov 18, 2021
6e465ea
Merge pull request #42 from mjmeli/bump-pydukeenergy-0.0.12
mjmeli Nov 18, 2021
1985d1d
bump version v0.1.0b6
mjmeli Nov 18, 2021
9c2d561
delete the remove subscriber funcs from the dictionary instead of set…
mjmeli Dec 3, 2021
924a924
tick pyduke_energy to 0.0.13
mjmeli Dec 3, 2021
3e684cf
tick integration version 0.1.0b7
mjmeli Dec 3, 2021
65ee79b
tick pyduke-energy to v0.0.14
mjmeli Dec 3, 2021
dcf7a35
Merge pull request #46 from mjmeli/0.1.0b7
mjmeli Dec 3, 2021
7911dd4
Merge remote-tracking branch 'origin/main' into 0.1.0-beta
mjmeli Dec 16, 2021
49cabb5
tick version 0.1.0b8
mjmeli Dec 16, 2021
f811c36
integrate to pyduke-energy 1.0.0 (has breaking changes) to use new ru…
mjmeli Dec 16, 2021
6a04d8a
Merge pull request #53 from mjmeli/pyduke-energy-1.0.0-forever-loop
mjmeli Dec 18, 2021
9bcc29b
increase home assistant min version
mjmeli Dec 20, 2021
1704086
fix issue with multiple update listeners on entry unload/reload
mjmeli Dec 22, 2021
d2ca8ae
Merge pull request #54 from mjmeli/fix-config-entry-reload
mjmeli Dec 22, 2021
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: 2 additions & 0 deletions .devcontainer/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ logger:
default: info
logs:
custom_components.duke_energy_gateway: debug
pyduke_energy.client: debug
pyduke_energy.realtime: debug
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
# debugpy:
57 changes: 48 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,27 @@
[![Project Maintenance][maintenance-shield]][user_profile]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]

This is a custom integration for [Home Assistant](https://www.home-assistant.io/). It pulls near-real-time energy usage from Duke Energy via the Duke Energy Gateway pilot program.
This is a custom integration for [Home Assistant](https://www.home-assistant.io/). It pulls real-time energy usage from Duke Energy via the Duke Energy Gateway pilot program.

This component will set up the following entities.
This integration leverages the [`pyduke-energy`](https://github.com/mjmeli/pyduke-energy) library, also written by me, to pull data. This API is _very_ unofficial and may stop working at any time (see [Disclaimer](https://github.com/mjmeli/pyduke-energy#Disclaimer)). Also, you are required to have a Duke Energy Gateway connected to your smartmeter for this to work. This integration does not support any other method of retrieving data (see [Gateway Requirement](https://github.com/mjmeli/pyduke-energy#gateway-requirement)).

| Platform | Description |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sensor.duke_energy_usage_today_kwh` | Represents today's energy consumption (from 0:00 local time to 23:59 local time, then resetting). Additional attributes are available containing the meter ID, gateway ID, and the timestamp of the last measurement. |
## Sensors

This integration leverages the [`pyduke-energy`](https://github.com/mjmeli/pyduke-energy) library, also written by me, to pull data. This API is _very_ unofficial and may stop working at any time (see [Disclaimer](https://github.com/mjmeli/pyduke-energy#Disclaimer)). Also, you are required to have a Duke Energy Gateway connected to your smartmeter for this to work. This integration does not support any other method of retrieving data (see [Gateway Requirement](https://github.com/mjmeli/pyduke-energy#gateway-requirement)).
This component will set up the following entities:

### `sensor.duke_energy_current_usage_w`

- Represents the real-time _power_ usage in watts.
- This data is pushed from the gateway device every 1-3 seconds. _NOTE:_ This produces a lot of data. If this update interval is too frequent for you, you can configure a throttling interval in seconds (see [Configuration](#Configuration) below).
- Note that since this is power usage, it cannot be used as-is for the Home Assistant energy dashboard. Instead, you can use the `sensor.duke_energy_usage_today_kwh` sensor, or you need to feed this real-time sensor through the [Riemann sum integral integration](https://www.home-assistant.io/integrations/integration/).
- Additional attributes are available containing the meter ID and gateway ID.

Energy usage will be provided as _daily_ data, resetting at midnight local time. At the moment, the API appears to be limited to providing new records every 15 minutes, meaning readings could be delayed up to 15 minutes. For more information, see [limitations](https://github.com/mjmeli/pyduke-energy#limitations) in the `pyduke-energy` repo.
### `sensor.duke_energy_usage_today_kwh`

- Represents today's _energy_ consumption in kilowatt-hours (from 0:00 local time to 23:59 local time, then resetting).
- This data is polled every 60 seconds, but data may be delayed up to 15 minutes due to delays in Duke Energy reporting it (see [Limitations](https://github.com/mjmeli/pyduke-energy#Limitations) in the `pyduke-energy` repo.).
- This can be used as-is for the Home Assistant energy consumption dashboard.
- Additional attributes are available containing the meter ID, gateway ID, and the timestamp of the last measurement.

## Installation

Expand All @@ -42,19 +52,40 @@ Energy usage will be provided as _daily_ data, resetting at midnight local time.
6. Restart Home Assistant
7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Duke Energy Gateway"

## Configuration is done in the UI
## Configuration

Configuration will be done in the UI. You will need to provide the following data:
Configuration will be done in the UI. Initially, you will need to provide the following data:

| Data | Description |
| ---------- | ----------------------------------- |
| `email` | Your login email to Duke Energy. |
| `password` | Your login password to Duke Energy. |

After the integration is setup, you will be able to do further configuration by clicking "Configure" on the integration page. This will allow you to modify the following options:

| Data | Description |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Real-time Usage Update Interval (sec)` | By default, the real-time usage sensor will be updated any time a reading comes in. If this data is too frequent, you can configure this value to throttle the data. When set to a positive integer `X`, the sensor will only be updated once every `X` seconds. In other words, if set to 30, you will get a new real-time usage every ~30 seconds. |

### Meter Selection

The configuration flow will automatically attempt to identify your gateway and smartmeter. Right now, only one is supported per account. The first one identified will be used. If one cannot be found, the configuration process should fail.

If your meter selection fails, a first step should be to enable logging for the component (see [Logging](#Logging)). If this does not give insight into the problem, please open a GitHub issue.

### Logging

If you run into any issues and want to look into the logs, this integration provides verbose logging at the debug level. That can be enabled by adding the following to your `configuration.yaml` file.

```yaml
logger:
default: info
logs:
custom_components.duke_energy_gateway: debug
pyduke_energy.client: debug
pyduke_energy.realtime: debug
```

## Development

I suggest using the dev container for development by opening in Visual Studio Code with `code .` and clicking on the option to re-open with dev container. In VS Code, you can run the task "Run Home Assistant on the port 9123" and then access it via http://localhost:9123.
Expand All @@ -63,6 +94,14 @@ If you want to install manually, you can install dev dependencies with `pip inst

Before commiting, run `pre-commit run --all-files`.

### Working With In Development `pyduke-energy` Versions

If you are working on implementing new changes from `pyduke-energy` but do not want to release version of that library, you can set up your development environment to install from a remote working branch.

1. Update [`requirements_dev.txt`](requirements_dev.txt) to replace the `main` in `git+https://github.com/mjmeli/pyduke-energy@main` with your working branch and update the username if you have a fork (e.g. `git+https://github.com/notmjmeli/pyduke-energy@new-feature-dev-branch`)
2. Uninstall locally cached version of `pyduke-energy`: `pip uninstall -y pyduke-energy`
3. Re-run requirements installation: `pip install -r requirements_dev.txt`

## Contributions are welcome!

If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)
Expand Down
122 changes: 29 additions & 93 deletions custom_components/duke_energy_gateway/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,22 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import UpdateFailed
from homeassistant.util import dt
from pyduke_energy.client import DukeEnergyClient
from pyduke_energy.realtime import DukeEnergyRealtime

from .const import CONF_EMAIL
from .const import CONF_PASSWORD
from .const import CONF_REALTIME_INTERVAL
from .const import CONF_REALTIME_INTERVAL_DEFAULT_SEC
from .const import DOMAIN
from .const import PLATFORMS
from .const import STARTUP_MESSAGE

SCAN_INTERVAL = timedelta(seconds=60)
from .coordinator import DukeEnergyGatewayUsageDataUpdateCoordinator

_LOGGER: logging.Logger = logging.getLogger(__package__)


async def async_setup(hass: HomeAssistant, config: Config):
async def async_setup(_hass: HomeAssistant, _config: Config):
"""Set up this integration using YAML is not supported."""
return True

Expand All @@ -42,22 +41,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):

email = entry.data.get(CONF_EMAIL)
password = entry.data.get(CONF_PASSWORD)
realtime_interval = entry.options.get(
CONF_REALTIME_INTERVAL, CONF_REALTIME_INTERVAL_DEFAULT_SEC
)

session = async_get_clientsession(hass)
client = DukeEnergyClient(email, password, session)
_LOGGER.debug("Setup Duke Energy API client")
realtime = DukeEnergyRealtime(client)
_LOGGER.debug("Set up Duke Energy API clients")

# Try to find the meter that is used for the gateway
selected_meter, selected_gateway = await find_meter_with_gateway(client)
# Find the meter that is used for the gateway
selected_meter, selected_gateway = await client.select_default_meter()

# If no meter was found, we raise an error
if not selected_meter or not selected_gateway:
if not selected_meter:
_LOGGER.error(
"Could not identify a smart meter on your account with gateway access."
)
return False

coordinator = DukeEnergyGatewayUsageDataUpdateCoordinator(hass, client=client)
coordinator = DukeEnergyGatewayUsageDataUpdateCoordinator(
hass,
client=client,
realtime=realtime,
realtime_interval=timedelta(seconds=realtime_interval),
)
await coordinator.async_refresh()

if not coordinator.last_update_success:
Expand All @@ -76,93 +84,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.config_entries.async_forward_entry_setup(entry, platform)
)

entry.add_update_listener(async_reload_entry)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
return True


async def find_meter_with_gateway(client: DukeEnergyClient):
"""Find the meter that is used for the gateway by iterating through the accounts and meters."""
account_list = await client.get_account_list()
account_numbers_text = ",".join([f"'{a.src_acct_id}'" for a in account_list])
_LOGGER.debug(
f"Accounts to check for gateway ({len(account_list)}): {account_numbers_text}"
)
for account in account_list:
try:
_LOGGER.debug(f"Checking account '{account.src_acct_id}' for gateway")
account_details = await client.get_account_details(account)
serial_numbers_text = ",".join(
[f"'{m.serial_num}'" for m in account_details.meter_infos]
)
_LOGGER.debug(
f"Meters to check for gateway ({len(account_details.meter_infos)}): {serial_numbers_text}"
)
for meter in account_details.meter_infos:
try:
_LOGGER.debug(
f"Checking meter '{meter.serial_num}' for gateway [meter_type={meter.meter_type}, is_certified_smart_meter={meter.is_certified_smart_meter}]"
)
if (
meter.serial_num
and meter.meter_type.upper() # sometimes blank meters show up
== "ELECTRIC"
and meter.is_certified_smart_meter
):
client.select_meter(meter)
gw_status = await client.get_gateway_status()
if gw_status is not None:
_LOGGER.debug(
f"Found meter '{meter.serial_num}' with gateway '{gw_status.id}'"
)
return meter, gw_status
else:
_LOGGER.debug(
f"No gateway status for meter '{meter.serial_num}'"
)
except Exception as e:
# Try the next meter if anything fails above
_LOGGER.debug(
f"Failed to check meter '{meter.serial_num}' on account '{account.src_acct_id}': {e}"
)
pass
except Exception as e:
# Try the next account if anything fails above
_LOGGER.debug(
f"Failed to find meter on account '{account.src_acct_id}': {e}"
)
pass
return None, None


class DukeEnergyGatewayUsageDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching usage data from the API."""

def __init__(
self,
hass: HomeAssistant,
client: DukeEnergyClient,
) -> None:
"""Initialize."""
self.api = client
self.platforms = []

super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)

async def _async_update_data(self):
"""Update data via library to get last day of minute-by-minute usage data."""
try:
today_start = dt.start_of_local_day()
today_end = today_start + timedelta(days=1)
return await self.api.get_gateway_usage(today_start, today_end)
except Exception as exception:
raise UpdateFailed(
f"Error communicating with Duke Energy Usage API: {exception}"
) from exception


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
coordinator: DukeEnergyGatewayUsageDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
]["coordinator"]

unloaded = all(
await asyncio.gather(
*[
Expand All @@ -175,6 +106,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unloaded:
hass.data[DOMAIN].pop(entry.entry_id)

# Cleanup real-time stream if it wasn't already done so (it should be done by the sensor entity)
_LOGGER.debug("Checking for clean-up of real-time stream in async_unload_entry")
coordinator.realtime_cancel()
coordinator.async_realtime_unsubscribe_all_from_dispatcher()

return unloaded


Expand Down
18 changes: 15 additions & 3 deletions custom_components/duke_energy_gateway/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

from .const import CONF_EMAIL
from .const import CONF_PASSWORD
from .const import CONF_REALTIME_INTERVAL
from .const import CONF_REALTIME_INTERVAL_DEFAULT_SEC
from .const import DOMAIN
from .const import PLATFORMS


class DukeEnergyGatewayFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
Expand Down Expand Up @@ -89,18 +90,29 @@ async def async_step_user(self, user_input=None):
self.options.update(user_input)
return await self._update_options()

realtime_interval = self.options.get(
CONF_REALTIME_INTERVAL, CONF_REALTIME_INTERVAL_DEFAULT_SEC
)

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(x, default=self.options.get(x, True)): bool
for x in sorted(PLATFORMS)
vol.Required(
CONF_REALTIME_INTERVAL,
default=realtime_interval,
): int,
}
),
)

async def _update_options(self):
"""Update config entry options."""
update_interval = self.options.get(
CONF_REALTIME_INTERVAL, CONF_REALTIME_INTERVAL_DEFAULT_SEC
)
if update_interval < 0:
return self.async_abort(reason="invalid_update_interval_value")
return self.async_create_entry(
title=self.config_entry.data.get(CONF_EMAIL), data=self.options
)
4 changes: 4 additions & 0 deletions custom_components/duke_energy_gateway/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@
CONF_ENABLED = "enabled"
CONF_EMAIL = "email"
CONF_PASSWORD = "password"
CONF_REALTIME_INTERVAL = "realtimeInterval"
CONF_REALTIME_INTERVAL_DEFAULT_SEC = 0 # no throttling

# Defaults
DEFAULT_NAME = DOMAIN

REALTIME_DISPATCH_SIGNAL = f"{DOMAIN}_realtime_dispatch_signal"

STARTUP_MESSAGE = f"""
-------------------------------------------------------------------
{NAME}
Expand Down
Loading