Skip to content

Commit

Permalink
[HomeWizard] Add support for Energy Socket and Watermeter products (o…
Browse files Browse the repository at this point in the history
…penhab#13495)

Also-by: Leo Siepel <[email protected]>
Signed-off-by: Daniël van Os <[email protected]>
  • Loading branch information
Daniel-42 authored and digitaldan committed Aug 29, 2024
1 parent 1280ee1 commit 93afc0e
Show file tree
Hide file tree
Showing 13 changed files with 789 additions and 132 deletions.
79 changes: 54 additions & 25 deletions bundles/org.openhab.binding.homewizard/README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
# HomeWizard Binding

The HomeWizard binding retrieves measurements from the HomeWizard Wi-Fi P1 meter.
The meter itself is attached to a DSMR Smart Meter and reads out the telegrams, which it will forward to cloud storage.
However, recently HomeWizard also added an interface that can be queried locally.
The HomeWizard binding provides access to several HomeWizard devices by using the local API of those devices.

This binding uses that local interface to make the measurements available.
## Installation

There are two important points of attention: the local API of each device must be enabled and a fixed address must be configured for the devices.

### Local API

The local API of a device can be enabled from the HomeWizard app.
Go to Settings in the app, then Meters and select the device you want to enable.
On this page enable the local API.

### Fixed Address

The devices support mDNS discovery but the binding does not support that yet.
As a result the devices should be reachable through a hostname or a fixed IP address.
Since the devices themselves have no option to set a fixed IP address you will need a different solution, for instance having your router hand out an IP address based upon the MAC address of the devices.

## Supported Things

The binding provides the P1 Meter thing.
The binding offers three Things, providing support for the P1 meter, the Watermeter and the Energy Socket.

| Thing | Device | Description |
|---------------|---------------------|---------------------------------------------------------------------------------------------------|
| p1_wifi_meter | Wi-Fi P1 Meter | Reads total and current energy usage and total gas usage. |
| energy_socket | Wi-Fi Energy Socket | Reads total and current energy usage. Controls power switch, lock and ring brightness. |
| watermeter | Wi-Fi Watermeter | Reads total and current water usage. |

The HomeWizard kWh meters are not yet officially supported, but they can probably be added as as 'p1_wifi_meter'. However, this has not been tested.

## Discovery

Auto discovery is not available for this binding.
Auto discovery is not yet available for this binding.

## Thing Configuration

The P1 Meter thing can be configured through the web interface.
All devices can be configured through the web interface.

| Parameter | Required | Default | Description |
|--------------|----------|---------|---------------------------------------------------------------------------------------------------|
Expand All @@ -26,28 +46,37 @@ The P1 Meter thing can be configured through the web interface.
Note that update rate of the P1 Meter itself depends on the frequency of the telegrams it receives from the Smart Meter.
For DSMR5 meters this is generally once per second, for older versions the frequency is much lower.

Example of configuration through a .thing file:
## Channels

| Channel ID | Item Type | Description |Available|
|------------------------|---------------------------|--------------------------------------------------------------------------------------------|---------|
| total_energy_import_t1 | Number:Energy | The most recently reported total imported energy in kWh by counter 1. | P,E |
| total_energy_import_t2 | Number:Energy | The most recently reported total imported energy in kWh by counter 2. | P |
| total_energy_export_t1 | Number:Energy | The most recently reported total exported energy in kWh by counter 1. | P,E |
| total_energy_export_t2 | Number:Energy | The most recently reported total exported energy in kWh by counter 2. | P |
| active_power | Number:Power | The current net total power in W. It will be below 0 if power is currently being exported. | P,E |
| active_power_l1 | Number:Power | The current net total power in W for phase 1. | P |
| active_power_l2 | Number:Power | The current net total power in W for phase 2. | P |
| active_power_l3 | Number:Power | The current net total power in W for phase 3. | P |
| total_gas | Number:Volume | The most recently reported total imported gas in m^3. | P |
| gas_timestamp | DateTime | The time stamp of the total_gas measurement. | P |
| total_water | Number:Volume | Total water used. | W |
| current_water | Number:VolumetricFlowRate | Current water usage. | W |
| power_switch | Switch | Controls the power switch of the socket. | E |
| power_lock | Switch | Controls the lock of the power switch (un/locking both the API and the physical button) | E |
| ring_brightness | Number:Dimensionless | Controls the brightness of the ring on the socket | E |

## Full Example

### `homewizard.things` Example

```java
Thing homewizard:p1_wifi_meter:my_meter [ ipAddress="192.178.1.67", refreshDelay=5 ]
Thing homewizard:p1_wifi_meter:my_p1 [ ipAddress="192.178.1.67", refreshDelay=5 ]
Thing homewizard:energy_socket:my_socket [ ipAddress="192.178.1.61", refreshDelay=5 ]
Thing homewizard:watermeter:my_water [ ipAddress="192.178.1.27", refreshDelay=15 ]
```

## Channels

| Channel ID | Item Type | Description |
|------------------------|---------------|--------------------------------------------------------------------------------------------|
| total_energy_import_t1 | Number:Energy | The most recently reported total imported energy in kWh by counter 1. |
| total_energy_import_t2 | Number:Energy | The most recently reported total imported energy in kWh by counter 2. |
| total_energy_export_t1 | Number:Energy | The most recently reported total exported energy in kWh by counter 1. |
| total_energy_export_t2 | Number:Energy | The most recently reported total exported energy in kWh by counter 2. |
| active_power | Number:Power | The current net total power in W. It will be below 0 if power is currently being exported. |
| active_power_l1 | Number:Power | The current net total power in W for phase 1. |
| active_power_l2 | Number:Power | The current net total power in W for phase 2. |
| active_power_l3 | Number:Power | The current net total power in W for phase 3. |
| total_gas | Number:Volume | The most recently reported total imported gas in m^3. |
| gas_timestamp | DateTime | The time stamp of the total_gas measurement. |

Example of configuration through a .items file:
### `homewizard.items` Example

```java
Number:Energy Energy_Import_T1 "Imported Energy T1 [%.0f kWh]" {channel="homewizard:p1_wifi_meter:my_meter:total_energy_import_t1" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
import com.google.gson.annotations.SerializedName;

/**
* Class that provides storage for the json object obtained from the P1 meter API
* Class that provides storage for the json objects obtained from HomeWizard devices.
*
* @author Daniël van Os - Initial contribution
*
*/
@NonNullByDefault
public class P1Payload {
public class DataPayload {
private int smrVersion = 0;
private String meterModel = "";
private String wifiSsid = "";
Expand All @@ -45,6 +45,11 @@ public class P1Payload {
private double totalGasM3;
private long gasTimestamp = 0;

@SerializedName("total_liter_m3")
private double totalWaterM3;
@SerializedName("active_liter_lpm")
private double currentWaterLPM;

/**
* Getter for the smart meter version
*
Expand Down Expand Up @@ -297,15 +302,53 @@ public void setGasTimestamp(long gasTimestamp) {
this.gasTimestamp = gasTimestamp;
}

/**
* Getter for the total imported water volume
*
* @return total imported water volume
*/
public double getTotalWaterM3() {
return totalWaterM3;
}

/**
* Setter for the total imported water volume
*
* @param totalWaterM3 total imported water volume
*/
public void setTotalWaterM3(double totalWaterM3) {
this.totalWaterM3 = totalWaterM3;
}

/**
* Getter for the current water flow
*
* @return current water flow
*/
public double getCurrentWaterLPM() {
return currentWaterLPM;
}

/**
* Setter for the current water flow
*
* @param currentWaterLPM current water flow
*/
public void setCurrentWaterLPM(double currentWaterLPM) {
this.currentWaterLPM = currentWaterLPM;
}

@Override
public String toString() {
return String.format(
"""
P1 [version: %d model: %s ssid: %s signal: %d\
imp1: %f imp2: %f exp1: %f exp2: %f active: %f active1: %f active2: %f active3: %f gas: %f timestamp: %.0f]\
Data [smrVersion: %d meterModel: %s wifiSsid: %s wifiStrength: %d"
totalEnergyImportT1Kwh: %f totalEnergyImportT2Kwh: %f totalEnergyExportT1Kwh: %f totalEnergyExportT2Kwh: %f"
activePowerW: %f activePowerL1W: %f activePowerL2W: %f activePowerL3W: %f totalGasM3: %f gasTimestamp: %.0f"
totalWaterM3: %f currentWaterLPM: %f]
""",
smrVersion, meterModel, wifiSsid, wifiStrength, totalEnergyImportT1Kwh, totalEnergyImportT2Kwh,
totalEnergyExportT1Kwh, totalEnergyExportT2Kwh, activePowerW, activePowerL1W, activePowerL2W,
activePowerL3W, totalGasM3, gasTimestamp);
activePowerL3W, totalGasM3, gasTimestamp, totalWaterM3, currentWaterLPM);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public class HomeWizardBindingConstants {
private static final String BINDING_ID = "homewizard";

// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_P1_WIFI_METER = new ThingTypeUID(BINDING_ID, "p1_wifi_meter");
public static final ThingTypeUID THING_TYPE_P1_METER = new ThingTypeUID(BINDING_ID, "p1_wifi_meter");
public static final ThingTypeUID THING_TYPE_ENERGY_SOCKET = new ThingTypeUID(BINDING_ID, "energy_socket");
public static final ThingTypeUID THING_TYPE_WATERMETER = new ThingTypeUID(BINDING_ID, "watermeter");

// List of all Channel ids
public static final String CHANNEL_ENERGY_IMPORT_T1 = "total_energy_import_t1";
Expand All @@ -40,6 +42,12 @@ public class HomeWizardBindingConstants {
public static final String CHANNEL_ACTIVE_POWER_L3 = "active_power_l3";
public static final String CHANNEL_TOTAL_GAS = "total_gas";
public static final String CHANNEL_GAS_TIMESTAMP = "gas_timestamp";
public static final String CHANNEL_TOTAL_WATER = "total_water";
public static final String CHANNEL_CURRENT_WATER = "current_water";

public static final String CHANNEL_POWER_SWITCH = "power_switch";
public static final String CHANNEL_POWER_LOCK = "power_lock";
public static final String CHANNEL_RING_BRIGHTNESS = "ring_brightness";

public static final String PROPERTY_METER_MODEL = "meterModel";
public static final String PROPERTY_METER_VERSION = "meterVersion";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
public class HomeWizardConfiguration {

/**
* IP Address or host for the P1 Meter
* IP Address or host for a HomeWizard device.
*/
public String ipAddress = "";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.homewizard.internal;

import java.io.IOException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

/**
* The {@link HomeWizardDeviceHandler} is a base class for all
* HomeWizard devices. It provides configuration and polling of
* data from a device. It also processes common data.
*
* @author Daniël van Os - Initial contribution
*/
@NonNullByDefault
public abstract class HomeWizardDeviceHandler extends BaseThingHandler {

protected final Logger logger = LoggerFactory.getLogger(HomeWizardDeviceHandler.class);
protected final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();

private HomeWizardConfiguration config = new HomeWizardConfiguration();
private @Nullable ScheduledFuture<?> pollingJob;

protected String apiURL = "";

/**
* Constructor
*
* @param thing The thing to handle
*/
public HomeWizardDeviceHandler(Thing thing) {
super(thing);
}

/**
* If a host has been specified start polling it
*/
@Override
public void initialize() {
config = getConfigAs(HomeWizardConfiguration.class);
if (configure()) {
pollingJob = scheduler.scheduleWithFixedDelay(this::pollingCode, 0, config.refreshDelay, TimeUnit.SECONDS);
}
}

/**
* Check the current configuration
*
* @return true if the configuration is ok to start polling, false otherwise
*/
private boolean configure() {
if (config.ipAddress.trim().isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Missing ipAddress/host configuration");
return false;
} else {
updateStatus(ThingStatus.UNKNOWN);
apiURL = String.format("http://%s/api/v1/", config.ipAddress.trim());
return true;
}
}

/**
* dispose: stop the poller
*/
@Override
public void dispose() {
var job = pollingJob;
if (job != null) {
job.cancel(true);
}
pollingJob = null;
}

/**
* Device specific handling of the returned data payload.
*
* @param payload The data parsed from the data Json file
*/
abstract protected void handleDataPayload(DataPayload payload);

/**
*
*/
protected void pollData() {
final String dataResult;

try {
dataResult = HttpUtil.executeUrl("GET", apiURL + "data", 30000);
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
String.format("Unable to query device data: %s", e.getMessage()));
return;
}

if (dataResult.trim().isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device returned empty data");
return;
}

DataPayload dataPayload = gson.fromJson(dataResult, DataPayload.class);
if (dataPayload == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Unable to parse data response from device");
return;
}

if ("".equals(dataPayload.getWifiSsid())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Results from API are empty");
return;
}

updateStatus(ThingStatus.ONLINE);
handleDataPayload(dataPayload);
}

/**
* The actual polling loop
*/
protected void pollingCode() {
pollData();
}
}
Loading

0 comments on commit 93afc0e

Please sign in to comment.