Skip to content
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
12 changes: 1 addition & 11 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,5 @@ API Reference
:members:
:undoc-members:
:show-inheritance:
:inherited-members:

Submodules
__________

.. autosummary::
:toctree: submodules
:recursive:

devices
plug_api
virtual_household
xlatemsg
3 changes: 3 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,6 @@
"Miscellaneous",
),
]

# We don't need module names prefixed everywhere; we're only one module
add_module_names = False
61 changes: 56 additions & 5 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,62 @@ Here's a simple example of how to use powersensor_local:

.. code-block:: python

import powersensor_local
import asyncio
import logging
import signal

# Add your usage examples here
from powersensor_local import PowersensorZeroconfDevices, Event
from types import FrameType

Advanced Usage
--------------
# Exceptions escaping the callback handler can be logged
logger = logging.getLogger(__name__)

More detailed examples and advanced features will be documented here.
# Create a zeroconf-based devices view, with logging enabled
devs = PowersensorZeroconfDevices(logger = logger)

# Sentinel for exiting cleanly
exiting = False

# All events from (and pertaining to) Powersensor devices end up here
async def handle_event(ev: Event) -> None:
event = ev.get("event")
mac = ev.get("mac")
match event:
case "device_found":
print(f"Device found: {mac}")
# Subscribe to events from this device
devs.subscribe(str(mac))
case "device_lost":
print(f"Device disappeared: {mac}")
# Unsubscription happens automatically
case "average_power":
print(f"Power ({mac}): {ev['watts']}")
# other events...

# Main sequence startup
async def main() -> None:
# Commence mDNS discovery
await devs.start(handle_event)
# Everything happen asynchronously, so wait until time to exit
while not exiting:
await asyncio.sleep(0.5)
# Be nice and unsubscribe and disconnect on exit
await devs.stop()

# Signal handler to flag that it's time to exit
def flag_exit(signum: int, frame: FrameType) -> None:
global exiting
exiting = True
signal.signal(signal.SIGINT, signal.SIG_DFL)

if __name__ == "__main__":
# Register ^C handler for graceful exit
signal.signal(signal.SIGINT, flag_exit)
# Run the main with an event loop
asyncio.run(main())

Further reading
---------------

Refer to the example utilities `zc_events.py`, `events.py`, `plugevents.py`,
and `rawplug.py` for additional usage examples.
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ description = "Network-local (non-cloud) interface for Powersensor devices"
authors = [
{ name = "Jade Mattsson", email = "jmattsson@dius.com.au" },
{ name = "Lake Bookman", email = "lbookman@dius.com.au" },
{ name = "Joo Aun Saw", email = "jasaw@dius.com.au" },
]
readme = "README.md"
requires-python = ">=3.11"
requires-python = ">=3.12"
classifiers = [
"Programming Language :: Python :: 3",
"Intended Audience :: Developers",
Expand Down Expand Up @@ -36,9 +37,9 @@ zeroconf = [
"zeroconf>=0.38.0",
]
docs = [
"sphinx>=7.0.0",
"sphinx-rtd-theme>=1.3.0",
"sphinx-autodoc-typehints>=1.24.0",
"sphinx>=9.0.0",
"sphinx-rtd-theme>=3.1.0",
"sphinx-autodoc-typehints>=3.13.9",
]

[tool.hatch.version]
Expand Down
61 changes: 42 additions & 19 deletions src/powersensor_local/__init__.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
"""Direct (non-cloud) interface to Powersensor devices
"""Direct (non-cloud) interface to Powersensor devices.

This package contains various abstractions for interacting with Powersensor
devices on the local network.

The library provides two types of abstractions: per-plug event streams, and a
shared-across-all-devices event stream. Neither is "best", it depends on the
particular use case. Device discovery can be done in two ways. The preferred
way is via mDNS/Zeroconf. There is also a legacy method relying on a simpler,
custom, discovery protocol, but there should be no reason to use it for new
projects.

The simplest approach is to use the shared event stream, and do discovery via
Zeroconf. This is available out-of-the-box in the
:class:`PowersensorZeroconfDevices` class, which encapsulates the Zeroconf
machinations and merging of plug event streams.

A legacy abstraction is also provided via
:class:`PowersensorLegacyDevices`, which uses an older way of discovering plugs.
It is more fragile than Zeroconf, but may be slightly more responsive in
certain situations, as the user is fully in control over when to scan for
devices. In general it is not recommended for new use.

If wanting individual event streams from each plug, the :class:`PlugApi` class
is the one to use. Device discovery is left up to the user in this case.

The recommended approach is to use mDNS to discover plugs via their service
"_powersensor._udp.local" (or "_powersensor._tcp.local" for TCP transport), and
then instantiate a PlugApi to obtain the event stream from each plug. Note
that the plugs are only capable of handling a single TCP connection at a time,
so UDP is the preferred transport. Up to 5 concurrent subscriptions are
supported over UDP. The interfaces provided by PlugListenerUdp and
PlugListenerTCP are identical; switching between them should be trivial.

A legacy abstraction is also provided via PowersensorDevices, which uses
an older way of discovering plugs, and then funnels all the event streams
through a single callback.

Lower-level interfaces are available in the PlugListenerUdp and PlugListenerTcp
classes, though they are not recommended for general use.
supported over UDP. Internally, the :class:`PlugApi` uses either
:class:`PlugListenerUdp` or :class:`PlugListenerTcp` for connecting to the
plugs. The interfaces are identical; switching between UDP and TCP should be
invisible to the user of the :class:`PlugApi`.

Additionally, a convenience abstraction for translating some of the events into
a household view is available in VirtualHousehold.
a household view is available in :class:`VirtualHousehold`.

Quick overview:
- PlugApi is the recommended API layer
- PlugListenerUdp is the UDP lower-level abstraction used by PlugApi
- PlugListenerTcp is the TCP lower-level abstraction used by PlugApi
- PowersensorDevices is the legacy main API layer
- LegacyDiscovery provides access to the legacy discovery mechanism
- VirtualHousehold can be used to translate events into a household view
- :class:`PowersensorZeroconfDevices` is the shared event stream abstraction with Zeroconf discovery.
- :class:`PowersensorLegacyDevices` provides the legacy discovery mechanism together with a shared event stream.
- :class:`PlugApi` is the recommended API layer for per-plug event streams.
- :class:`PlugListenerUdp` is the UDP lower-level abstraction used by PlugApi.
- :class:`PlugListenerTcp` is the TCP lower-level abstraction used by PlugApi.
- :class:`VirtualHousehold` can be used to translate events into a household view.

The 'plugevents' and 'rawplug' modules are helper utilities provided as
debug aids, which get installed under the names ps-plugevents and ps-rawplug
Expand All @@ -36,7 +52,7 @@
Similarly, 'zc_events' offers up events from PowersensorZeroconfDevices,
installed as 'ps-zcevents', again, for debug use only.

Type hints for emitted data are available in Event/Events/Message.
Type hints for emitted data are available in :type:`Event`/:type:`Events`/:type:`Message`.
"""
__all__ = [
'VirtualHousehold',
Expand All @@ -47,16 +63,23 @@
'PowersensorDevices',
'PowersensorLegacyDevices',
'PowersensorZeroconfDevices',
'AsyncEventCallback',
'Event',
'Events',
'Message',
'CommonEventsDocs',
]
__version__ = "2.4.0rc3"
from .devices import PowersensorDevices, PowersensorLegacyDevices
from .devices import (
AsyncEventCallback,
PowersensorDevices,
PowersensorLegacyDevices
)
from .legacy_discovery import LegacyDiscovery
from .plug_api import PlugApi
from .plug_listener_tcp import PlugListenerTcp
from .plug_listener_udp import PlugListenerUdp
from .virtual_household import VirtualHousehold
from .zeroconf_devices import PowersensorZeroconfDevices
from .xlatemsg import Event, Events, Message
from .common_events_docs import CommonEventsDocs
2 changes: 1 addition & 1 deletion src/powersensor_local/async_event_emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
from typing import Any, Awaitable, Callable

_OpenCallback = Callable[..., Awaitable[None]]
type _OpenCallback = Callable[..., Awaitable[None]]

class AsyncEventEmitter:
"""Small helper class for pub/sub functionality with async handlers.
Expand Down
168 changes: 168 additions & 0 deletions src/powersensor_local/common_events_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
class CommonEventsDocs:
"""
The following is a list of common events that may be emitted.

The first few are discovery events, while the rest are data events.

**device_found**
A device has been discovered or re-discovered.

Note that due to device hardware limitations, role information is NOT
reliably available at this time, and therefore not included in this
message. There are situations where role information will never become
available, and therefore the API can make no promises otherwise. If a
user requires role information, they must manage that themselves and
also provide a mechanism for handling the situation of a device not
being able to provide role information in the first place. If a device
does supply a role at any point, it should be considered authoritative
and override any user provided value. The event data comprises:

- "event": "device_found"
- "mac": The MAC address of the device.
- "device_type": One of "plug" or "sensor".

**device_lost**
A device has definitively disappeared (plug) or been inactive long
enough to expire (sensor). The event data comprises:

- "event": "device_lost"
- "mac": The MAC address of the device.

**average_power**
An event denoting the average power seen over a short duration, such
as 1 or 30 seconds typically. Both plugs and powersensors may originate
these. The event data comprises:

- "event": "average_power"
- "mac": The MAC address of the device.
- "role": The assigned role of the device, if known.
- "via": The id of the relaying plug, if from a sensor.
- "starttime_utc": Seconds since the Unix Epoch, in UTC.
- "duration_s": The number of seconds the reading is calculated over.
- "watts": The average power, in Watts. May be negative for e.g.
solar sensors and house sensors when exporting solar to the grid.

**average_power_components**
An plug-only event providing additional information on the power
components feeding into the power measurement. To be read in
conjunction with the "average_power" message with the same
starttime_utc value. Comprises:

- "event": "average_power_components"
- "mac": The MAC address of the device.
- "role": The assigned role of the device, if known.
- "starttime_utc": Seconds since the Unix Epoch, in UTC.
- "apparent_current": The apparent current, in Amperes.
- "active_current": The active current component, in Amperes.
- "reactive_current": The reactive current component, in Amperes.
- "volts": The mains voltage as seen by the plug, in Volts.

**summation_energy**
An event reporting an energy summation value. Issued for both plugs
and sensors. Comprises:

- "event": "summation_energy"
- "mac": The MAC address of the device.
- "role": The assigned role of the device, if known.
- "via": The id of the relaying plug, if from a sensor.
- "starttime_utc": Seconds since the Unix Epoch, in UTC.
- "summation_joules": The summation value, in Joules (Watt seconds).
This value may go backwards (and even become negative) if solar
export is present. The summation may increment or decrement
depending on whether energy is being imported from or exported
to the grid.
- "summation_resettime_utc": A timestamp denoting the last time the
summation value (may have) reset. The initial value on a reset is
NOT zero. Summation values from before the reset time can not be
safely compared or diffed with ones from after the reset time.
The format is the same as for "starttime_utc" — seconds since the
Unix Epoch, in UTC.

**average_flow**
An event denoting the average flow rate seen over a short duration,
such as 1 or 30 seconds typically. Only originated by water sensors.
The event data comprises:

- "event": "average_flow"
- "mac": The MAC address of the device.
- "role": The assigned role of the device, if known.
- "via": The id of the relaying plug.
- "starttime_utc": Seconds since the Unix Epoch, in UTC.
- "duration_s": The number of seconds the reading is calculated over.
- "litres_per_minute": The average flow detected, in litres/min..

**summation_volume**
An event reporting a volume summation value. Issued only for water
sensors. Comprises:

- "event": "summation_volume"
- "mac": The MAC address of the device.
- "role": The assigned role of the device, if known.
- "via": The id of the relaying plug.
- "starttime_utc": Seconds since the Unix Epoch, in UTC.
- "summation_litres": The summation value, in litres.
- "summation_resettime_utc": A timestamp denoting the last time the
summation value (may have) reset. The initial value on a reset is
NOT zero. Summation values from before the reset time can not be
safely compared or diffed with ones from after the reset time.
The format is the same as for "starttime_utc" — seconds since the
Unix Epoch, in UTC.

**uncalibrated_average_reading**
An event denoting an reading with unknown unit, for an average over a
short duration, such as 1 or 30 seconds typically. Issued by
powersensors, prior to calibration. The event data comprises:

- "event": "uncalibrated_average_reading"
- "mac": The MAC address of the device.
- "role": The assigned role of the device, if known.
- "via": The id of the relaying plug.
- "starttime_utc": Seconds since the Unix Epoch, in UTC.
- "duration_s": The number of seconds the reading is calculated over.
- "value": The value, in unspecified units. The only valid thing
to use this value for is display it relative to other uncalibrated
values from the same device. The magnitude of the value is an
indication of the strength of the signal seen by the sensor, but
the unit is most definitely NOT in Watts. For most purposes, this
event can (and should be) ignored. Once the backend has successfully
calibrated the sensor against one or more plugs, these events
will be replaced by "average_power" events instead.

**battery_level**
An event reporting the battery level of a sensor. The event data
comprises:

- "event": "battery_level"
- "mac": The MAC address of the device.
- "role": The assigned role of the device, if known.
- "via": The id of the relaying plug.
- "starttime_utc": Seconds since the Unix Epoch, in UTC.
- "volts": The current battery level, in Volts. Sensors operate
on 3.7V nominally, with a fully charged battery at around 4.2V.
Precise battery curves vary individually. It is intentional that
we do not attempt to map these to a percentage value here, as
between individual differences and environmental conditions they
are bound to be inaccurate. Short of characterising each battery
in its environment, any such mapping will be inaccurate. It can
be argued that users wishing a simple percentage display are best
off using a simple linear extrapolation across the middle part of
the curve, e.g. 3.3V and 4.15V. It's not entirely accurate, but
it's also not useless.

**radio_signal_quality**
An event reporting radio signal quality for a sensor. Note that this
is for the long-range radio comms with the plug, not for the WiFi
signal. The event comprises:

- "event": "radio_signal_quality"
- "mac": The MAC address of the device.
- "role": The assigned role of the device, if known.
- "via": The id of the relaying plug.
- "starttime_utc": Seconds since the Unix Epoch, in UTC.
- "duration_s": The number of seconds the reading is calculated over.
- "average_rssi": The average of the RSSI in messages received
during the reporting window. Values below -90 are considered poor
reception.
- "last_rssi": The most recent RSSI value.
"""
pass
Loading
Loading