Skip to content

Commit

Permalink
HomeWizard support for P1-meter (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mausy5043 authored Dec 1, 2024
2 parents 4c48184 + 58d9cea commit 637ab94
Show file tree
Hide file tree
Showing 13 changed files with 704 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version: 2
updates:
- package-ecosystem: 'pip'
directory: '/'
target-branch: 'homewiz'
target-branch: 'master'
schedule:
interval: 'weekly'
allow:
Expand Down
27 changes: 27 additions & 0 deletions bin/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,33 @@
},
"template_keys_to_drop": ["yr", "mon", "dom", "hr", "min"],
}

WIZ_P1: dict = {
"database": _DATABASE,
"sql_table": "mains",
"sql_command": "INSERT INTO mains ("
"sample_time, sample_epoch, "
"T1in, T2in, powerin, "
"T1out, T2out, powerout, "
"tarif, swits"
");"
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"report_interval": 900,
"samplespercycle": 15,
"delay": 0,
"template": {
"sample_time": "dd-mmm-yyyy hh:mm:ss",
"sample_epoch": 0,
"T1in": 0,
"T2in": 0,
"powerin": 0,
"T1out": 0,
"T2out": 0,
"powerout": 0,
"tarif": 1,
"swits": 1,
},
}
# fmt: on


Expand Down
8 changes: 4 additions & 4 deletions bin/include.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ declare -a lektrix_timers=("lektrix.trend.day.timer"
"lektrix.trend.year.timer")
# "lektrix.update.timer" (incl. the .service) is not installed
# list of services provided
declare -a lektrix_services=("lektrix.kamstrup.service"
declare -a lektrix_services=("lektrix.wizp1.service"
"lektrix.myenergi.service"
"lektrix.solaredge.service")
# Install python3 and develop packages
Expand Down Expand Up @@ -254,16 +254,16 @@ action_services() {
# See if packages are installed and install them using apt-get
action_apt_install() {
PKG=$1
echo "*********************************************************"
echo "***************************************************(APT)*"
echo "* $app_name running on $host_name requesting ${PKG}"
status=$(dpkg -l | awk '{print $2}' | grep -c -e "^${PKG}*")
if [ "${status}" -eq 0 ]; then
echo -n "* Installing ${PKG} "
sudo apt-get -yqq install "${PKG}" && echo " ... [OK]"
echo "*********************************************************"
echo "***************************************************(APT)*"
else
echo "* Already installed !!!"
echo "*********************************************************"
echo "***************************************************(APT)*"
fi
echo
}
Expand Down
194 changes: 194 additions & 0 deletions bin/libwizp1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#!/usr/bin/env python3

# lektrix
# Copyright (C) 2024 Maurice (mausy5043) Hendrix
# AGPL-3.0-or-later - see LICENSE

# https://api-documentation.homewizard.com/docs/category/api-v1

"""Common functions for use with the Home Wizard P1 electricity meter dongle using the API/v1"""

# import asyncio
import datetime as dt
import logging
import sys

import constants
import numpy as np
import pandas as pd
from homewizard_energy import HomeWizardEnergyV1
from libzeroconf import discover as zcd

LOGGER: logging.Logger = logging.getLogger(__name__)


# https://api-documentation.homewizard.com/docs/category/api-v1


class WizP1_v1: # pylint: disable=too-many-instance-attributes
"""Class to interact with the Home Wizard P1-dongle."""

def __init__(
self, debug: bool = False
) -> None: # pylint: disable=too-many-instance-attributes

# get a HomeWizard IP
_howip = zcd.get_ip("_hwenergy")

if _howip:
self.ip = _howip[0]
self.dt_format = constants.DT_FORMAT # "%Y-%m-%d %H:%M:%S"
# starting values
self.electra1in = np.nan
self.electra2in = np.nan
self.electra1out = np.nan
self.electra2out = np.nan
self.powerin = np.nan
self.powerout = np.nan
self.tarif = 1
self.swits = 0
self.list_data: list = []

self.debug: bool = debug
self.firstcall = True
if debug:
if len(LOGGER.handlers) == 0:
LOGGER.addHandler(logging.StreamHandler(sys.stdout))
LOGGER.level = logging.DEBUG
LOGGER.debug("Debugging on.")
self.telegram: list = []

async def get_telegram(self):
"""Fetch a telegram from the serialport.
Returns:
(bool): valid telegram received True or False
"""
async with HomeWizardEnergyV1(host=self.ip) as _api:
if self.debug and self.firstcall:
# Get device information, like firmware version
wiz_dev = await _api.device()
LOGGER.debug(wiz_dev)
LOGGER.debug("")
self.firstcall = False

# Get measurements
wiz_data = await _api.data()
LOGGER.debug(wiz_data)
LOGGER.debug("---")

self.list_data.append(self._translate_telegram(wiz_data))
LOGGER.debug(self.list_data)
LOGGER.debug("*-*")

def _translate_telegram(self, telegram) -> dict:
"""Translate the telegram to a dict.
kW or kWh are converted to W resp. kW
Returns:
(dict): data converted to a dict.
"""

# telegram will look something like this:
#
# Data(wifi_ssid='wifiSSID', wifi_strength=72, smr_version=None,
# meter_model='KamstrupKA_______', unique_meter_id=' KA________',
# active_tariff=2,
# total_energy_import_kwh=37736.315,
# total_energy_import_t1_kwh=24001.234, total_energy_import_t2_kwh=13735.081,
# total_energy_import_t3_kwh=None, total_energy_import_t4_kwh=None,
# total_energy_export_kwh=11438.238,
# total_energy_export_t1_kwh=3195.724, total_energy_export_t2_kwh=8242.514,
# total_energy_export_t3_kwh=None, total_energy_export_t4_kwh=None,
# active_power_w=760.0,
# active_power_l1_w=None, active_power_l2_w=None, active_power_l3_w=None,
# active_voltage_v=None,
# active_voltage_l1_v=None, active_voltage_l2_v=None, active_voltage_l3_v=None,
# active_current_a=None,
# active_current_l1_a=None, active_current_l2_a=None, active_current_l3_a=None,
# active_apparent_power_va=None,
# active_apparent_power_l1_va=None, active_apparent_power_l2_va=None,
# active_apparent_power_l3_va=None,
# active_reactive_power_var=None,
# active_reactive_power_l1_var=None, active_reactive_power_l2_var=None,
# active_reactive_power_l3_var=None,
# active_power_factor=None,
# active_power_factor_l1=None, active_power_factor_l2=None, active_power_factor_l3=None,
# active_frequency_hz=None,
# voltage_sag_l1_count=None, voltage_sag_l2_count=None, voltage_sag_l3_count=None,
# voltage_swell_l1_count=None, voltage_swell_l2_count=None, voltage_swell_l3_count=None,
# any_power_fail_count=None, long_power_fail_count=None, active_power_average_w=None,
# monthly_power_peak_w=None, monthly_power_peak_timestamp=None,
# total_gas_m3=None, gas_timestamp=None, gas_unique_id=None,
# active_liter_lpm=None, total_liter_m3=None,
# external_devices={})

self.electra1in = int(telegram.total_energy_import_t1_kwh * 1000)
self.electra2in = int(telegram.total_energy_import_t2_kwh * 1000)
self.electra1out = int(telegram.total_energy_export_t1_kwh * 1000)
self.electra2out = int(telegram.total_energy_export_t2_kwh * 1000)
self.tarif = telegram.active_tariff
self.powerin = telegram.active_power_w
self.powerout = 0.0
self.swits = 1
if self.powerin < 0.0:
self.swits = 0
self.powerout = self.powerin
self.powerin = 0.0

idx_dt: dt.datetime = dt.datetime.now()
epoch = int(idx_dt.timestamp())

return {
"sample_time": idx_dt.strftime(self.dt_format),
"sample_epoch": epoch,
"T1in": self.electra1in,
"T2in": self.electra2in,
"powerin": self.powerin,
"T1out": self.electra1out,
"T2out": self.electra2out,
"powerout": self.powerout,
"tarif": self.tarif,
"swits": self.swits,
}

def compact_data(self, data) -> tuple:
"""
Compact the ten-second data into 15-minute data
Args:
data (list): list of dicts containing 10-second data from the electricity meter
Returns:
(list): list of dicts containing compacted 15-minute data
"""

def _convert_time_to_epoch(date_to_convert) -> int:
return int(pd.Timestamp(date_to_convert).timestamp())

def _convert_time_to_text(date_to_convert) -> str:
return str(pd.Timestamp(date_to_convert).strftime(constants.DT_FORMAT))

df = pd.DataFrame(data)
df = df.set_index("sample_time")
df.index = pd.to_datetime(df.index, format=constants.DT_FORMAT, utc=False)
# resample to monotonic timeline
df_out = df.resample("15min", label="right").max()
# df_mean = df.resample("15min", label="right").mean()

df_out["powerin"] = df_out["powerin"].astype(int)
df_out["powerout"] = df_out["powerout"].astype(int)
# recreate column 'sample_time' that was lost to the index
df_out["sample_time"] = df_out.index.to_frame(name="sample_time")
df_out["sample_time"] = df_out["sample_time"].apply(_convert_time_to_text)

# recalculate 'sample_epoch'
df_out["sample_epoch"] = df_out["sample_time"].apply(_convert_time_to_epoch)
result_data = df_out.to_dict("records") # list of dicts

df = df[df["sample_epoch"] > np.max(df_out["sample_epoch"])] # pylint: disable=E1136
remain_data = df.to_dict("records")
LOGGER.debug(f"Result: {result_data}")
LOGGER.debug(f"Remain: {remain_data}\n")
return result_data, remain_data
3 changes: 3 additions & 0 deletions bin/libzeroconf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .discover import get_ip

__all__ = ["get_ip"]
Loading

0 comments on commit 637ab94

Please sign in to comment.