[Senseenergy] Initial contribution of SenseEnergy Binding (#18244)
* Initial contribution of SenseEnergy Binding Signed-off-by: Jeff James <jeff@james-online.com>pull/18114/merge
parent
7b67b1cbd3
commit
4fc8f75639
|
@ -338,6 +338,7 @@
|
|||
/bundles/org.openhab.binding.senechome/ @vctender @KorbinianP @eguib
|
||||
/bundles/org.openhab.binding.seneye/ @nikotanghe
|
||||
/bundles/org.openhab.binding.sensebox/ @hakan42
|
||||
/bundles/org.openhab.binding.senseenergy/ @jsjames
|
||||
/bundles/org.openhab.binding.sensibo/ @seime
|
||||
/bundles/org.openhab.binding.sensorcommunity/ @weymann
|
||||
/bundles/org.openhab.binding.serial/ @MikeJMajor
|
||||
|
|
|
@ -1666,6 +1666,11 @@
|
|||
<artifactId>org.openhab.binding.sensebox</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.senseenergy</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.sensibo</artifactId>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
|
@ -0,0 +1,243 @@
|
|||
# SenseEnergy Binding
|
||||
|
||||
This binding supports the Sense Energy monitor (sense.com) which can be used to provide accurate energy usage statistics in the home.
|
||||
In addition to providing real-time and historical energy production and usage data, Sense also uses machine learning to predict specific device usage.
|
||||
This binding interfaces with the Sense cloud and local monitor to provide openHAB real-time energy updates for the whole house and any known devices.
|
||||
Note, the binding currently assumes (and the configuration tested) that the monitor is installed in a solar enabled configuration.
|
||||
If you have a device in a different configuration, feedback would be welcome and support might be added to the binding.
|
||||
|
||||
Sense also allows smart plugs and other devices to provide real-time usage allowing Sense to incorporate accurate usage statistics into your data.
|
||||
This binding also supports creation of virtual proxy devices in openHAB which can send to Sense any energy usage for openHAB aware devices.
|
||||
Here are several examples of how a virtual proxy device can be used:
|
||||
|
||||
- A pool pump known in openHAB can update its power when on to Sense
|
||||
- A switch or dimmer where the power is known can get send to Sense when on.
|
||||
- A fan / scene represented as a String can be used to notify Sense of the power usage when a particualar scene is on.
|
||||
|
||||
Using the openHAB follow profile with the channel(s) of the proxy device is an easy way to link these proxy devices to your openHAB setup. (see examples)
|
||||
|
||||
This binding builds off the following works in understanding the Sense API:
|
||||
|
||||
- https://github.com/scottbonline/sense
|
||||
- https://github.com/cbpowell/SenseLink/
|
||||
|
||||
## Supported Things
|
||||
|
||||
| Thing Id | Label | Type | Description |
|
||||
|------------------|------------------------------|--------|--------------------------------------------------------------------|
|
||||
| cloud-connector | Sense Energy Cloud Connector | Bridge | This represents the cloud account to interface with the Sense Ener |
|
||||
| monitor | Sense Monitor Device | Bridge | This interfaces to a specific Sense energy monitor associated with |
|
||||
| proxy-device | Virtual Device Emulation | Device | This is a proxy device used to provide real-time usage to Sense. |
|
||||
|
||||
## Discovery
|
||||
|
||||
Initial configurtion involes creating a cloud-connector device with the email and password for the Sense cloud account.
|
||||
Once the cloud-connector has been created and initialized, the monitor(s) associated with the account will be auto-discovered.
|
||||
Virtual proxy devices are created manually attached to the monitor bridge.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
### Sense Energy Cloud Connector
|
||||
|
||||
The `cloud-connector` is configured with the email and password for your Sense Energy cloud account.
|
||||
At present, the binding does not support multi-factor authentication which should be disabled via the Sense app settings.
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|----------|------|--------------------------------------|---------|----------|----------|
|
||||
| email | text | email for the Sense cloud account | N/A | yes | no |
|
||||
| password | text | Password for the Sense cloud account | N/A | yes | no |
|
||||
|
||||
### Sense Monitor Device
|
||||
|
||||
The monitor will be auto-discovered after the `cloud-connector` bridge goes online.
|
||||
The only configuration parameter is the id, however, this is not available via the Sense app or sense.com.
|
||||
When supporting textual configuration, you can monitor the openhab.log in order to see the id for your monitor device.
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|------|---------|---------------------------|---------|----------|----------|
|
||||
| id | integer | ID for the monitor device | N/A | yes | no |
|
||||
|
||||
### Virtual Proxy Device Emulation
|
||||
|
||||
Virtual proxy devices can be created in order to notify Sense of specific power usage of devices in your home.
|
||||
These emulate a TP-Link Kasa HS110 smart plug and will report to Sense the power usage based on on the configuration of the proxy device and the state.
|
||||
In order to use, you need to enable "TP-Link HS110/HS300 Smart Plug" in the Sense app.
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|-------------|-----------------|-----------------------------------------------|---------|----------|----------|
|
||||
| powerLevels | text | Power levels for different states. | N/A | no | no |
|
||||
| voltage | decimal | Voltage level for the proxy device. | 120 | yes | no |
|
||||
| mac | network-address | A spoof'ed MAC address for this proxy device. | random | no | yes |
|
||||
|
||||
|
||||
#### Power Levels
|
||||
|
||||
The power levels is a list representing different power levels or states of the device.
|
||||
Here are several examples of how this can be configured:
|
||||
|
||||
| powerLevel parameter | Description | Example Device |
|
||||
|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|
|
||||
| 10 W | The device is a simple ON/OFF switch or Dimmer with 0 W in the OFF or 0% state, and 10 W in the full ON or 100% state. For a dimmer, the power will be linearly interpolated over the full range [0 W, 10 W] depending on current dim level. | Light |
|
||||
| 800 mW,10 W | The device is a simple ON/OFF switch or Dimmer with 800 mW in the OFF or 0% state, and 10 W in the full ON or 100% state. For a dimmer, the power will be linearly interpolated over the full range [800 mW, 10 W] depending on current dim level. | TV which standby power > 0 W |
|
||||
| 0 W,1 W,3 W,8 W, 15 W | A device which has non-linear power usage at different dim levels. This configuration would use 0 W at 0%, 1 W at 25%, 3 W at 50%, 8 W at 75% and 15 W at the full 100%. Other levels are linearly interpolated within the bounding points of the sub-range. | Dimmable light with non-linear usage profile |
|
||||
| OFF=0 W,LOW=200 W,HIGH=400 W | A device with several power states with different power levels in state represented by a String. | A fan with OFF, LOW, HIGH states |
|
||||
|
||||
#### MAC
|
||||
|
||||
Each proxy device must be configured with a MAC address.
|
||||
The virtual device creates a random MAC address which is used in identification of the device to Sense.
|
||||
Note, if configuring via the textual interace, it is important to provide the MAC field, otherwise a different MAC address will be randomized whenever openHAB restarts and the proxy device will appear as a new additional device to Sense.
|
||||
|
||||
## Channels
|
||||
|
||||
### Cloud-Connector
|
||||
|
||||
There are no channels associated with the cloud-connector bridge.
|
||||
|
||||
### Monitor
|
||||
|
||||
The monitor channels are organized into multiple groups.
|
||||
|
||||
#### General Channel Group
|
||||
|
||||
| Channel Id | Label | Type | Read/Write | Description |
|
||||
|-------------|-------------|----------------------------|------------|---------------------------------------------------------------------------------|
|
||||
| frequency | Frequency | Number:Frequency | R | Electrical frequency detected by Sense. |
|
||||
| grid-power | Grid Power | Number:Power | R | Power consumed from the grid (negative if supplying power to grid). |
|
||||
| potential-1 | Potential 1 | Number:ElectricalPotential | R | Potential measured on first 120V branch. |
|
||||
| potential-2 | Potential 2 | Number:ElectricalPotential | R | Potential measured on second 120V branch. |
|
||||
| main-power | Main Power | Number:Power | R | Power detected by the main Sense clamp (only present in solar mode). |
|
||||
| solar-power | Solar Power | Number:Power | R | Power detected by the solar Sense clamp (only present in solar mode). |
|
||||
| leg-1-power | Leg 1 Power | Number:Power | R | Power detected by the first Sense clamp (only present when not in solar mode). |
|
||||
| leg-2-power | Leg 2 Power | Number:Power | R | Power detected by the second Sense clamp (only present when not in solar mode). |
|
||||
|
||||
#### Discovered Devices, Self-Reporting Devices and Proxy Devices Channel Groups
|
||||
|
||||
- Discovered devices are those which Sense has discovered using their algorithms.
|
||||
- Self-reporting devices are any devices which report their power usage to Sense (i.e. energy reporting smart plugs).
|
||||
- Proxy devices are any virtual proxy devices set up in openHAB where this binding reports their power usage.
|
||||
|
||||
| Channel | id | Type | Read/Write | Advanced | Description |
|
||||
|-------------------------|-------------------|--------------|------------|----------|------------------------------------------------------------------|
|
||||
| *Label*: Power | *id*-device-power | Number:Power | R | N | Power consumed by the device. |
|
||||
| *Label*: On Off Trigger | *id*-trigger | Trigger | N/A | Y | Trigger channel to notify when device has been turned ON or OFF. |
|
||||
|
||||
### Proxy Device
|
||||
|
||||
Each proxy device has several channels that can be used to notify Sense of the current power usage for the device.
|
||||
These can either attached to an openHAB item, or, can be used with the system:follow profile to follow the state of another channel (see example).
|
||||
|
||||
| Channel | id | Type | Read/Write | Description |
|
||||
|---------- |-------- |-------- |-------- |--------------- |
|
||||
| Power Level | proxy-device-power | Number:Power | W | Sets a specific absolute real-time power usage for the device. |
|
||||
| Device Switch | proxy-device-switch | Switch | W | Sets the power level to either the ON or OFF defined in the powerLevels parameter. |
|
||||
| Device Dimmer | proxy-device-dimmer | Dimmer | W | Sets the power level to an interpolated value based on the powerLevels parameter. |
|
||||
| Device State | proxy-device-state | String | W | Sets the power level to the state sepecifice in the powerLevels parameter. |
|
||||
|
||||
## Full Example
|
||||
|
||||
### `demo.things` Example
|
||||
|
||||
```java
|
||||
Bridge senseenergy:cloud-connector:cloud [ email="xxx", password="xxx" ] {
|
||||
Bridge monitor monitor1 [ id=869850 ] {
|
||||
Thing proxy-device poolpump "Sense Virtual Pool Pump"
|
||||
Thing proxy-device light "Sense Virtual Light" [ powerLevels="20 W" ]
|
||||
Thing proxy-device fan "Sense Virtual Fan" [ powerLevels="OFF=0 W,LOW=10 W, HIGH=20 W" ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `demo.items` Example
|
||||
|
||||
```java
|
||||
// General channels
|
||||
Number:Frequency Main_Frequency "Main Frequency" { channel="senseenergy:monitor:cloud:monitor1:general#frequency" }
|
||||
Number:Power To_Grid_Power "To Grid Power" { channel="senseenergy:monitor:cloud:monitor1:general#grid-power" }
|
||||
Number:ElectricPotential Branch_1_Potential "Branch 1 Potential" { channel="senseenergy:monitor:cloud:monitor1:general#potential-1" }
|
||||
Number:ElectricPotential Branch_2_Potential "Branch 2 Potential" { channel="senseenergy:monitor:cloud:monitor2:general#potential-2" }
|
||||
Number:Power Main_Power "Main Power" { channel="senseenergy:monitor:cloud:monitor1:general#main-power" }
|
||||
Number:Power Solar_Power "Solar Power" { channel="senseenergy:monitor:cloud:monitor1:general#solar-power" }
|
||||
|
||||
// Discovered device channels
|
||||
Number:Power Sense_AlwaysOn_Power "Always-On Power" { channel="senseenergy:monitor:cloud:monitor1:discovered-devices#always_on-device-power" }
|
||||
Number:Power Sense_PoolPump_Power "Pool Pump Power" { channel="senseenergy:monitor:cloud:monitor1:discovered-devices#Z0sBBkO1-device-power" }
|
||||
Number:Power Sense_Other_Power "Other Power" { channel="senseenergy:monitor:cloud:monitor1:discovered-devices#unknown-device-power" }
|
||||
|
||||
// Virtual proxy device "follow" channels. These should be the actually controlling items for your device.
|
||||
Switch LightSwitch "Light Switch" { channel="senseenergy:proxy-device:cloud:monitor1:light:proxy-device-switch"[profile="system:follow"] }
|
||||
Dimmer LightDimmer "Light Dimmer" { channel="senseenergy:proxy-device:cloud:monitor1:light:proxy-device-dimmer"[profile="system:follow"] }
|
||||
Number:Power PoolPump_Power "Pool Pump Power" { channel="senseenergy:proxy-device:cloud:monitor1:light:proxy-device-power"[profile="system:follow"] }
|
||||
String Fan_State "Fan State" { channel="senseenergy:proxy-device:cloud:monitor1:light:proxy-device-state"[profile="system:follow"] }
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
```java
|
||||
rule "Sense Energy Discovered Device OnOff"
|
||||
when
|
||||
Channel 'senseenergy:monitor:cloud:monitor1:discovered-devices#XXXX-trigger' triggered or
|
||||
Channel 'senseenergy:monitor:cloud:monitor1:self-reporting-devices#YYYY-trigger' triggered or
|
||||
Channel 'senseenergy:monitor:cloud:monitor1:proxy-devices#ZZZZ-trigger' triggered
|
||||
then
|
||||
logInfo("SenseEnergy", "Sense Energy device turned ON/OFF - Event: {}", receivedEvent)
|
||||
end
|
||||
```
|
||||
|
||||
### Rule Actions
|
||||
|
||||
The binding also supports querying of trend totals over a periods of time.
|
||||
|
||||
#### Map<String, Object> queryEnergyTrend(String scale, Instant datetime)
|
||||
|
||||
This function will query tthe Sense cloud for usage totals for a given period of time.
|
||||
|
||||
##### Parameters
|
||||
|
||||
`scale` - the time scale for which the query should be over ("DAY", "WEEK", "MONTH", "YEAR").
|
||||
`datetime` - the datetime in the period. this can be null to select the current datetime.
|
||||
|
||||
##### Returns
|
||||
|
||||
The return is a Map<String, Object> object which contains the following values:
|
||||
|
||||
`consumption` - a QuantityType<Energy> of the total energy (KWh) used over the scale period.
|
||||
`production` - a QuantityType<Energy> of the total energy (KWh) produced over the scale period.
|
||||
`fromGrid` - a QuantityType<Energy> of the total energy (KWh) from the grid over the scale period.
|
||||
`toGrid` - a QuantityType<Energy> of the total energy (KWh) to the grid over the scale period.
|
||||
`netProduction` - a QuantityType<Energy> of the difference in energy (KWh) between what was produced and consumed during the scale period.
|
||||
`solarPowered` - a QuantityType<Dimensionless> of the percent of solar energy production that was directly consumed (not sent to grid) during the scale period.
|
||||
|
||||
##### Example
|
||||
|
||||
```java
|
||||
rule "Sense Energy Update Trends"
|
||||
when
|
||||
Time cron "0 0/15 * ? * *"
|
||||
then
|
||||
logInfo("SenseEnergy", "Sense Energy Update Trends")
|
||||
|
||||
val monitorActions = getActions("senseenergy", "senseenergy:monitor:cloud:monitor1")
|
||||
|
||||
val dayTrends = monitorActions.queryEnergyTrend("DAY", null)
|
||||
logInfo("SenseEnergy", "Energy DAY trends {}", dayTrends.toString())
|
||||
|
||||
val weekTrends = monitorActions.queryEnergyTrend("WEEK", null)
|
||||
logInfo("SenseEnergy", "Energy WEEK trends {}", weekTrends.toString())
|
||||
|
||||
val monthTrends = monitorActions.queryEnergyTrend("MONTH", null)
|
||||
logInfo("SenseEnergy", "Energy MONTH trends {}", monthTrends.toString())
|
||||
|
||||
val yearTrends = monitorActions.queryEnergyTrend("YEAR", null)
|
||||
logInfo("SenseEnergy", "Energy YEAR trends {}", yearTrends.toString())
|
||||
end
|
||||
```
|
||||
|
||||
## Special Notes
|
||||
|
||||
For proxy device to work, openHAB must be on the same sub-net as the Sense monitor and be able to receive broadcast Datagram packets on port 9999.
|
||||
While the binding has not been tested in a Docker configuration, there are some potential issues with being able to receive on port 9999 (see https://github.com/cbpowell/SenseLink/).
|
||||
|
||||
The Sense Energy Monitor can be configured in two different modes depending on whether the secondary current monitor is either attaced to the Solar circuit of another circuit in your house.
|
||||
Unfortunately, the JSON format from the API is different depending on the mode and currently the binding has only been tested and will work in the Solar mode.
|
||||
If there are others wanting to use the setup in a different mode, I would be interested in enabling support.
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.senseenergy</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: SenseEnergy Binding</name>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.senseenergy-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-senseenergy" description="SenseEnergy Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.senseenergy/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.type.ChannelGroupTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyBindingConstants {
|
||||
private static final String BINDING_ID = "senseenergy";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "cloud-connector");
|
||||
public static final ThingTypeUID MONITOR_THING_TYPE = new ThingTypeUID(BINDING_ID, "monitor");
|
||||
public static final ThingTypeUID PROXY_DEVICE_THING_TYPE = new ThingTypeUID(BINDING_ID, "proxy-device");
|
||||
|
||||
public static final String PARAM_MONITOR_ID = "id";
|
||||
|
||||
public static final int HEARTBEAT_MINUTES = 5;
|
||||
|
||||
/** Monitor Bridge/Thing ***/
|
||||
// Channel group type UIDs
|
||||
public static final ChannelGroupTypeUID CHANNEL_GROUP_TYPE_DEVICE_TEMPLATE = new ChannelGroupTypeUID(BINDING_ID,
|
||||
"device-template");
|
||||
|
||||
// Channel Groups
|
||||
public static final String CHANNEL_GROUP_GENERAL = "general";
|
||||
public static final String CHANNEL_GROUP_DISCOVERED_DEVICES = "discovered-devices";
|
||||
public static final String CHANNEL_GROUP_SELF_REPORTING_DEVICES = "self-reporting-devices";
|
||||
public static final String CHANNEL_GROUP_PROXY_DEVICES = "proxy-devices";
|
||||
|
||||
// Monitor Channel IDs
|
||||
public static final String CHANNEL_FREQUENCY = "frequency";
|
||||
public static final String CHANNEL_GRID_POWER = "grid-power";
|
||||
public static final String CHANNEL_POTENTIAL_1 = "potential-1";
|
||||
public static final String CHANNEL_POTENTIAL_2 = "potential-2";
|
||||
public static final String CHANNEL_LEG_1_POWER = "leg-1-power";
|
||||
public static final String CHANNEL_LEG_2_POWER = "leg-2-power";
|
||||
public static final String CHANNEL_MAIN_POWER = "main-power";
|
||||
public static final String CHANNEL_SOLAR_POWER = "solar-power";
|
||||
public static final String CHANNEL_DEVICES_UPDATED_TRIGGER = "devices-updated-trigger";
|
||||
|
||||
// Discovered Device Channel IDs
|
||||
public static final String CHANNEL_DEVICE_POWER = "device-power";
|
||||
public static final String CHANNEL_DEVICE_TRIGGER = "device-trigger";
|
||||
|
||||
// Properties
|
||||
public static final String PROPERTY_MONITOR_SOLAR_CONFIGURED = "solarConfigured";
|
||||
public static final String PROPERTY_MONITOR_IP_ADDRESS = "ipAddress";
|
||||
public static final String PROPERTY_MONITOR_VERSION = "version";
|
||||
public static final String PROPERTY_MONITOR_SERIAL = "serial";
|
||||
public static final String PROPERTY_MONITOR_SSID = "ssid";
|
||||
public static final String PROPERTY_MONITOR_MAC = "mac";
|
||||
|
||||
/** PROXY DEVICE THING ***/
|
||||
// Channel IDs
|
||||
public static final String CHANNEL_PROXY_DEVICE_POWER = "proxy-device-power";
|
||||
public static final String CHANNEL_PROXY_DEVICE_SWITCH = "proxy-device-switch";
|
||||
public static final String CHANNEL_PROXY_DEVICE_DIMMER = "proxy-device-dimmer";
|
||||
public static final String CHANNEL_PROXY_DEVICE_STATE = "proxy-device-state";
|
||||
|
||||
public static final String CONFIG_PARAMETER_MAC = "mac";
|
||||
public static final String CONFIG_PARAMETER_POWER_LEVELS = "powerLevels";
|
||||
public static final String CONFIG_PARAMETER_SENSE_NAME = "senseName";
|
||||
|
||||
public static final String ACTION_OUTPUT_CONSUMPTION = "consumption";
|
||||
public static final String ACTION_OUTPUT_PRODUCTION = "production";
|
||||
public static final String ACTION_OUTPUT_FROM_GRID = "fromGrid";
|
||||
public static final String ACTION_OUTPUT_TO_GRID = "toGrid";
|
||||
public static final String ACTION_OUTPUT_NET_PRODUCTION = "netProduction";
|
||||
public static final String ACTION_OUTPUT_SOLAR_POWERED = "solarPowered";
|
||||
public static final String ACTION_INPUT_SCALE = "scale";
|
||||
public static final String ACTION_INPUT_DATETIME = "datetime";
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal;
|
||||
|
||||
import static org.openhab.binding.senseenergy.internal.SenseEnergyBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.senseenergy.internal.handler.SenseEnergyBridgeHandler;
|
||||
import org.openhab.binding.senseenergy.internal.handler.SenseEnergyMonitorHandler;
|
||||
import org.openhab.binding.senseenergy.internal.handler.SenseEnergyProxyDeviceHandler;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.openhab.core.thing.type.ChannelGroupTypeRegistry;
|
||||
import org.openhab.core.thing.type.ChannelTypeRegistry;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyHandlerFactory}
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.senseenergy", service = ThingHandlerFactory.class)
|
||||
public class SenseEnergyHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(APIBRIDGE_THING_TYPE, MONITOR_THING_TYPE,
|
||||
PROXY_DEVICE_THING_TYPE);
|
||||
|
||||
private final HttpClientFactory httpClientFactory;
|
||||
private final WebSocketFactory webSocketFactory;
|
||||
private final ChannelGroupTypeRegistry channelGroupTypeRegistry;
|
||||
private final ChannelTypeRegistry channelTypeRegistry;
|
||||
|
||||
@Activate
|
||||
public SenseEnergyHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
|
||||
final @Reference WebSocketFactory webSocketFactory,
|
||||
final @Reference ChannelGroupTypeRegistry channelGroupTypeRegistry,
|
||||
final @Reference ChannelTypeRegistry channelTypeRegistry) {
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
this.webSocketFactory = webSocketFactory;
|
||||
this.channelGroupTypeRegistry = channelGroupTypeRegistry;
|
||||
this.channelTypeRegistry = channelTypeRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (APIBRIDGE_THING_TYPE.equals(thingTypeUID)) {
|
||||
return new SenseEnergyBridgeHandler((Bridge) thing, this.httpClientFactory.getCommonHttpClient());
|
||||
} else if (MONITOR_THING_TYPE.equals(thingTypeUID)) {
|
||||
return new SenseEnergyMonitorHandler((Bridge) thing, webSocketFactory.getCommonWebSocketClient(),
|
||||
channelGroupTypeRegistry, channelTypeRegistry);
|
||||
} else if (PROXY_DEVICE_THING_TYPE.equals(thingTypeUID)) {
|
||||
return new SenseEnergyProxyDeviceHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.actions;
|
||||
|
||||
import static org.openhab.binding.senseenergy.internal.SenseEnergyBindingConstants.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
import javax.measure.quantity.Energy;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyApi;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyApiException;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiGetTrends;
|
||||
import org.openhab.binding.senseenergy.internal.handler.SenseEnergyMonitorHandler;
|
||||
import org.openhab.core.automation.annotation.ActionInput;
|
||||
import org.openhab.core.automation.annotation.ActionOutput;
|
||||
import org.openhab.core.automation.annotation.RuleAction;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.binding.ThingActions;
|
||||
import org.openhab.core.thing.binding.ThingActionsScope;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The { @link SenseEnergyMonitorActions } class implements the action(s) methods for the binding.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@Component(scope = ServiceScope.PROTOTYPE, service = SenseEnergyMonitorActions.class)
|
||||
@ThingActionsScope(name = "senseenergy")
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyMonitorActions implements ThingActions {
|
||||
private final Logger logger = LoggerFactory.getLogger(SenseEnergyMonitorActions.class);
|
||||
|
||||
private @Nullable SenseEnergyMonitorHandler deviceHandler;
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
if (handler instanceof SenseEnergyMonitorHandler deviceHandler) {
|
||||
this.deviceHandler = deviceHandler;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return deviceHandler;
|
||||
}
|
||||
|
||||
/*
|
||||
* Query water usage
|
||||
*/
|
||||
@RuleAction(label = "Query Energy Trend", description = "@text/actions.description.query-energy-trend")
|
||||
public @ActionOutput(name = ACTION_OUTPUT_CONSUMPTION, type = "QuantityType<Energy>", description = "@text/actions.output.description.consumption") //
|
||||
@ActionOutput(name = ACTION_OUTPUT_PRODUCTION, type = "QuantityType<Energy>", description = "@text/actions.output.description.production") //
|
||||
@ActionOutput(name = ACTION_OUTPUT_FROM_GRID, type = "QuantityType<Energy>", description = "@text/actions.output.description.from-grid") //
|
||||
@ActionOutput(name = ACTION_OUTPUT_TO_GRID, type = "QuantityType<Energy>", description = "@text/actions.output.description.to-grid") //
|
||||
@ActionOutput(name = ACTION_OUTPUT_NET_PRODUCTION, type = "QuantityType<Energy>", description = "@text/actions.output.description.net-production") //
|
||||
@ActionOutput(name = ACTION_OUTPUT_SOLAR_POWERED, type = "QuantityType<Dimensionless>", description = "@text/actions.output.description.solar-powered") //
|
||||
Map<String, QuantityType<?>> queryEnergyTrend( //
|
||||
@ActionInput(name = ACTION_INPUT_SCALE, label = "Scale", required = true, description = "@text/actions.input.description.scale") @Nullable String scale, //
|
||||
@ActionInput(name = ACTION_INPUT_DATETIME, label = "Date/Time", required = true, description = "@text/actions.input.description.datetime") @Nullable Instant datetime) {
|
||||
logger.info("queryEnergyTrend called");
|
||||
|
||||
SenseEnergyMonitorHandler localDeviceHandler = deviceHandler;
|
||||
if (localDeviceHandler == null) {
|
||||
logger.warn("querying device usage, but device is undefined.");
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Instant localDateTime = (datetime == null) ? Instant.now() : datetime;
|
||||
|
||||
if (scale == null) {
|
||||
logger.warn("queryEnergyTrends called with null inputs");
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
if (!SenseEnergyApi.TrendScale.contains(scale)) {
|
||||
logger.warn("Invalid scale type in call to queryEnergyTrend");
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
SenseEnergyApi.TrendScale trendScale = SenseEnergyApi.TrendScale.valueOf(scale);
|
||||
|
||||
SenseEnergyApiGetTrends trends;
|
||||
try {
|
||||
trends = localDeviceHandler.getApi().getTrendData(localDeviceHandler.getId(), trendScale, localDateTime);
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) {
|
||||
logger.warn("queryEnergyTrends function failed - {}", e.getMessage());
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
if (trends == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<String, QuantityType<?>> valuesMap = new HashMap<>();
|
||||
|
||||
valuesMap.put(ACTION_OUTPUT_CONSUMPTION,
|
||||
new QuantityType<Energy>(trends.consumption.totalPower, Units.KILOWATT_HOUR));
|
||||
valuesMap.put(ACTION_OUTPUT_PRODUCTION,
|
||||
new QuantityType<Energy>(trends.production.totalPower, Units.KILOWATT_HOUR));
|
||||
valuesMap.put(ACTION_OUTPUT_TO_GRID, new QuantityType<Energy>(trends.toGridEnergy, Units.KILOWATT_HOUR));
|
||||
valuesMap.put(ACTION_OUTPUT_FROM_GRID, new QuantityType<Energy>(trends.fromGridEnergy, Units.KILOWATT_HOUR));
|
||||
valuesMap.put(ACTION_OUTPUT_NET_PRODUCTION,
|
||||
new QuantityType<Energy>(trends.netProduction, Units.KILOWATT_HOUR));
|
||||
valuesMap.put(ACTION_OUTPUT_SOLAR_POWERED, new QuantityType<Dimensionless>(trends.solarPowered, Units.PERCENT));
|
||||
|
||||
return valuesMap;
|
||||
}
|
||||
|
||||
// Static method for Rules DSL backward compatibility
|
||||
public static @Nullable Map<String, QuantityType<?>> queryEnergyTrend(ThingActions actions, @Nullable String scale,
|
||||
@Nullable Instant datetime) {
|
||||
if (actions instanceof SenseEnergyMonitorActions localActions) {
|
||||
return localActions.queryEnergyTrend(scale, datetime);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not a SenseEnergyMonitorActions class.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,403 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.FormContentProvider;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiAuthenticate;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiDevice;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiGetTrends;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiMonitor;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiMonitorStatus;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiRefreshToken;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyApi} implements the api for sense energy cloud service. This is highly leveraged from the python
|
||||
* implementation here: https://github.com/scottbonline/sense
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyApi {
|
||||
private final Logger logger = LoggerFactory.getLogger(SenseEnergyApi.class);
|
||||
|
||||
private static final String APIURL_BASE = "https://api.sense.com/apiservice/api/v1/";
|
||||
private static final String APIURL_AUTHENTICATE = APIURL_BASE + "authenticate";
|
||||
private static final String APIURL_RENEW = APIURL_BASE + "renew";
|
||||
private static final String APIURL_MONITOR_STATUS = APIURL_BASE + "app/monitors/%s/status";
|
||||
private static final String APIURL_MONITOR_OVERVIEW = APIURL_BASE + "app/monitors/%s/overview";
|
||||
private static final String APIURL_GET_TRENDS = APIURL_BASE + "app/history/trends?monitor_id=%s&scale=%s&start=%s";
|
||||
private static final String APIURL_GET_DEVICES = APIURL_BASE + "app/monitors/%s/devices";
|
||||
private static final String APIURL_LOGOUT = APIURL_BASE + "logout";
|
||||
|
||||
private static final int API_TIMEOUT = 15;
|
||||
|
||||
private final Gson gson;
|
||||
|
||||
private String accessToken = "";
|
||||
// by experiment, the refresh period is 24 hours for initial authentication, on refresh, there is an expire field
|
||||
private static final TemporalAmount REFRESH_TOKEN_DURATION = Duration.ofMinutes(24 * 60 * 2 / 3);
|
||||
private String refreshToken = "";
|
||||
private long userID;
|
||||
private Instant tokenExpiresAt = Instant.MIN;
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
private JsonDeserializer<Instant> deseralizerInstant = (json, typeOfT, context) -> DateTimeFormatter.ISO_INSTANT
|
||||
.parse(json.getAsString(), Instant::from);
|
||||
|
||||
public enum TrendScale {
|
||||
DAY,
|
||||
WEEK,
|
||||
MONTH,
|
||||
YEAR;
|
||||
|
||||
public static boolean contains(String value) {
|
||||
return Arrays.stream(values()).anyMatch(t -> t.name().equals(value));
|
||||
}
|
||||
}
|
||||
|
||||
public SenseEnergyApi(final HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
this.gson = new GsonBuilder().registerTypeAdapter(Instant.class, deseralizerInstant).create();
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
/*
|
||||
* authenticates with the cloud api
|
||||
*
|
||||
* @param email
|
||||
*
|
||||
* @param password
|
||||
*
|
||||
* @return a set of IDs for all the monitors associated with this account
|
||||
*
|
||||
* @throws SenseEnergyApiException on authentication error
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*/
|
||||
public Set<Long> initialize(String email, String password)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
Fields fields = new Fields();
|
||||
fields.put("email", email);
|
||||
fields.put("password", password);
|
||||
|
||||
Request request = httpClient.newRequest(APIURL_AUTHENTICATE).method(HttpMethod.POST)
|
||||
.content(new FormContentProvider(fields));
|
||||
|
||||
ContentResponse response = sendRequest(request, false);
|
||||
|
||||
final SenseEnergyApiAuthenticate data = gson.fromJson(response.getContentAsString(),
|
||||
SenseEnergyApiAuthenticate.class);
|
||||
|
||||
if (data == null) {
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", false);
|
||||
}
|
||||
|
||||
accessToken = data.accessToken;
|
||||
refreshToken = data.refreshToken;
|
||||
userID = data.userID;
|
||||
tokenExpiresAt = Instant.now().plus(REFRESH_TOKEN_DURATION); // there is no expire field on the intial
|
||||
// authentication
|
||||
|
||||
return Stream.of(data.monitors).map(m -> m.id).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/*
|
||||
* renew authentication credentials. Timeout of credentials is ~24 hours.
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*
|
||||
* @throws SenseEnergyApiException
|
||||
*/
|
||||
public void refreshToken()
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
Fields fields = new Fields();
|
||||
fields.add("user_id", Long.toString(this.userID));
|
||||
fields.add("refresh_token", this.refreshToken);
|
||||
|
||||
Request request = httpClient.newRequest(APIURL_RENEW).method(HttpMethod.POST)
|
||||
.content(new FormContentProvider(fields));
|
||||
|
||||
ContentResponse response = sendRequest(request, false);
|
||||
|
||||
final SenseEnergyApiRefreshToken data = gson.fromJson(response.getContentAsString(),
|
||||
SenseEnergyApiRefreshToken.class);
|
||||
|
||||
if (data == null) {
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", false);
|
||||
}
|
||||
|
||||
logger.debug("Successful refreshToken {}", data.accessToken);
|
||||
|
||||
accessToken = data.accessToken;
|
||||
refreshToken = data.refreshToken;
|
||||
userID = data.userId;
|
||||
tokenExpiresAt = data.expires.minus(1, ChronoUnit.HOURS); // refresh an hour before token expires
|
||||
}
|
||||
|
||||
public void logout() throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
Request request = httpClient.newRequest(APIURL_LOGOUT).method(HttpMethod.GET);
|
||||
|
||||
sendRequest(request);
|
||||
}
|
||||
|
||||
/*
|
||||
* get overview of Monitor
|
||||
*
|
||||
* @param id of the monitor
|
||||
*
|
||||
* @return dto structure containing monitor info
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*
|
||||
* @throws SenseEnergyApiException
|
||||
*/
|
||||
public SenseEnergyApiMonitor getMonitorOverview(long id)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
String url = String.format(APIURL_MONITOR_OVERVIEW, id);
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
ContentResponse response = sendRequest(request);
|
||||
|
||||
try {
|
||||
JsonObject jsonResponse = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
SenseEnergyApiMonitor monitor = gson.fromJson(
|
||||
jsonResponse.getAsJsonObject("monitor_overview").getAsJsonObject("monitor"),
|
||||
SenseEnergyApiMonitor.class);
|
||||
|
||||
if (monitor == null) {
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", false);
|
||||
}
|
||||
|
||||
return monitor;
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new SenseEnergyApiException("@text/api.response-invalid", false);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* get monitor status
|
||||
*
|
||||
* @param id - id of monitor
|
||||
*
|
||||
* @return dto structure containing monitor status
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*
|
||||
* @throws SenseEnergyApiException
|
||||
*/
|
||||
@Nullable
|
||||
public SenseEnergyApiMonitorStatus getMonitorStatus(long id)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
String url = String.format(APIURL_MONITOR_STATUS, id);
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
ContentResponse response = sendRequest(request);
|
||||
|
||||
final SenseEnergyApiMonitorStatus data = gson.fromJson(response.getContentAsString(),
|
||||
SenseEnergyApiMonitorStatus.class);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SenseEnergyApiGetTrends getTrendData(long id, TrendScale trendScale)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
return getTrendData(id, trendScale, Instant.now());
|
||||
}
|
||||
|
||||
/*
|
||||
* get trend totals over a specific period
|
||||
*
|
||||
* @param id of monitor
|
||||
*
|
||||
* @param trendScale period of time over which to query data
|
||||
*
|
||||
* @param datetime a datetime within the scale of which to receive data. Does not need to be the start or end .
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*
|
||||
* @throws SenseEnergyApiException
|
||||
*/
|
||||
@Nullable
|
||||
public SenseEnergyApiGetTrends getTrendData(long id, TrendScale trendScale, Instant datetime)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
String url = String.format(APIURL_GET_TRENDS, id, trendScale.toString(), datetime.toString());
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
ContentResponse response = sendRequest(request);
|
||||
|
||||
final SenseEnergyApiGetTrends data = gson.fromJson(response.getContentAsString(),
|
||||
SenseEnergyApiGetTrends.class);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function to map jsonElement in stream. For some reason putting the gson conversion inline with the map
|
||||
* function yielded a @Nullable mismatch when build with maven
|
||||
*
|
||||
* @param jsonElement
|
||||
*
|
||||
* @return dto object extracted from the json
|
||||
*/
|
||||
@Nullable
|
||||
public SenseEnergyApiDevice jsonToSenseEnergyDevice(JsonElement jsonElement) {
|
||||
return gson.fromJson(jsonElement, SenseEnergyApiDevice.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* retrieves a Map of discovered devices
|
||||
*
|
||||
* @param id of the monitor device
|
||||
*
|
||||
* @return Map of discovered devices with the ID of the device as key and the dto object SenseEnergyApiDevice
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*
|
||||
* @throws ExecutionException
|
||||
*
|
||||
* @throws SenseEnergyApiException
|
||||
*/
|
||||
public Map<String, SenseEnergyApiDevice> getDevices(long id)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
String url = String.format(APIURL_GET_DEVICES, id);
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
ContentResponse response = sendRequest(request);
|
||||
|
||||
JsonArray jsonDevices = JsonParser.parseString(response.getContentAsString()).getAsJsonArray();
|
||||
|
||||
@SuppressWarnings("null") // prevent this warning on d.tags - [WARNING] Potential null pointer access: this
|
||||
// expression has
|
||||
// a '@Nullable' type
|
||||
Map<String, SenseEnergyApiDevice> mapDevices = StreamSupport.stream(jsonDevices.spliterator(), false) //
|
||||
.map(j -> jsonToSenseEnergyDevice(j)) //
|
||||
.filter(Objects::nonNull) //
|
||||
.filter(d -> (d.tags == null || !d.tags.userDeleted)) //
|
||||
.collect(Collectors.toMap(d -> Objects.requireNonNull(d.id), d -> Objects.requireNonNull(d)));
|
||||
|
||||
return mapDevices;
|
||||
}
|
||||
|
||||
private Request setHeaders(Request request) {
|
||||
request.header(HttpHeader.HOST, "api.sense.com");
|
||||
if (!accessToken.isEmpty()) {
|
||||
request.header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken);
|
||||
}
|
||||
request.timeout(API_TIMEOUT, TimeUnit.SECONDS);
|
||||
return request;
|
||||
}
|
||||
|
||||
public void verifyToken()
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
if (tokenExpiresAt.isBefore(Instant.now())) {
|
||||
refreshToken();
|
||||
}
|
||||
}
|
||||
|
||||
ContentResponse sendRequest(Request request)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
return sendRequest(request, true);
|
||||
}
|
||||
|
||||
ContentResponse sendRequest(Request request, boolean verifyToken)
|
||||
throws InterruptedException, TimeoutException, ExecutionException, SenseEnergyApiException {
|
||||
if (verifyToken) {
|
||||
verifyToken();
|
||||
}
|
||||
|
||||
setHeaders(request);
|
||||
|
||||
logger.trace("REQUEST: {}", request.toString());
|
||||
ContentResponse response = request.send();
|
||||
logger.trace("RESPONSE: {}", response.getContentAsString());
|
||||
|
||||
switch (response.getStatus()) {
|
||||
case 200:
|
||||
break;
|
||||
case 400: // API responses with 400 when user credentials are invalid
|
||||
case 401:
|
||||
throw new SenseEnergyApiException("@text/api.invalid-user-credentials", true);
|
||||
case 429:
|
||||
throw new SenseEnergyApiException("@text/api.rate-limit-exceeded", false);
|
||||
default:
|
||||
throw new SenseEnergyApiException("Unexpected API error: " + response.getReason(), false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyApiException} exception class for any api exception
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyApiException extends Exception {
|
||||
private static final long serialVersionUID = -7059398508028583720L;
|
||||
private final boolean configurationIssue;
|
||||
|
||||
public SenseEnergyApiException(String message, boolean configurationIssue) {
|
||||
super(message);
|
||||
this.configurationIssue = configurationIssue;
|
||||
}
|
||||
|
||||
public boolean isConfigurationIssue() {
|
||||
return configurationIssue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("SenseEnergyApiException{message='%s', configurationIssue=%b}", getMessage(),
|
||||
configurationIssue);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyDatagramGetRealtime;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyDatagramGetSysInfo;
|
||||
import org.openhab.binding.senseenergy.utils.TpLinkEncryption;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* @link { SenseEnergyDatagram } implements the datagram which receives and responds to local requests
|
||||
* from the sense monitor device querying realtime energy usage. Upon receiving a request for
|
||||
* energy usage, it will send request to the @link { packetListener } who is responsible for
|
||||
* calling @link { sendReponse } in order to respond.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyDatagram {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SenseEnergyDatagram.class);
|
||||
private static final int BUFFERSIZE = 1024;
|
||||
|
||||
private @Nullable DatagramSocket datagramSocket;
|
||||
private @Nullable SenseEnergyDatagramListener packetListener;
|
||||
private volatile boolean connected = false;
|
||||
private Gson gson = new Gson();
|
||||
@Nullable
|
||||
private Thread udpListener;
|
||||
|
||||
int port;
|
||||
String readerThreadName = "";
|
||||
|
||||
public SenseEnergyDatagram(SenseEnergyDatagramListener packetListener) {
|
||||
this.packetListener = packetListener;
|
||||
}
|
||||
|
||||
public synchronized void start(final int port, final String readerThreadName) throws IOException {
|
||||
if (this.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.port = port;
|
||||
this.readerThreadName = readerThreadName;
|
||||
|
||||
datagramSocket = new DatagramSocket(port);
|
||||
|
||||
Thread localUdpListener = new Thread(new UDPListener());
|
||||
localUdpListener.setName(readerThreadName);
|
||||
localUdpListener.setDaemon(true);
|
||||
localUdpListener.start();
|
||||
udpListener = localUdpListener;
|
||||
|
||||
this.connected = true;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
connected = false;
|
||||
|
||||
try {
|
||||
DatagramSocket localSocket = datagramSocket;
|
||||
if (localSocket != null) {
|
||||
localSocket.close();
|
||||
datagramSocket = null;
|
||||
logger.debug("Closing datagram listener");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
logger.debug("closeConnection(): Error closing connection - {}", exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void restart() throws IOException {
|
||||
stop();
|
||||
start(port, readerThreadName);
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
Thread localUDPListener = udpListener;
|
||||
return datagramSocket != null && connected && localUDPListener != null && localUDPListener.isAlive();
|
||||
}
|
||||
|
||||
private class UDPListener implements Runnable {
|
||||
|
||||
/*
|
||||
* Run method. Runs the MessageListener thread
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
long nextPacketTime = 0;
|
||||
logger.debug("Starting SenseEnergy Proxy Device Packet Receiver");
|
||||
|
||||
DatagramSocket localSocket = datagramSocket;
|
||||
|
||||
if (localSocket == null) {
|
||||
logger.error("UDPListener cannot start: Datagram socket is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
DatagramPacket packet = new DatagramPacket(new byte[BUFFERSIZE], BUFFERSIZE);
|
||||
|
||||
while (connected) {
|
||||
try {
|
||||
localSocket.receive(packet);
|
||||
} catch (IOException e) {
|
||||
logger.debug("Exception during packet read - {}", e.getMessage());
|
||||
try {
|
||||
Thread.sleep(100); // allow CPU to breath
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// don't receive more than 1 request a second. Necessary to filter out receiving the same
|
||||
// broadcast request packet on multiple interfaces (i.e. wi-fi and wired) at the same time
|
||||
if (System.nanoTime() < nextPacketTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonObject jsonResponse;
|
||||
String decryptedPacket = new String(TpLinkEncryption.decrypt(packet.getData(), packet.getLength()));
|
||||
try {
|
||||
jsonResponse = JsonParser.parseString(decryptedPacket).getAsJsonObject();
|
||||
} catch (JsonSyntaxException jsonSyntaxException) {
|
||||
logger.trace("Invalid JSON received");
|
||||
continue;
|
||||
}
|
||||
|
||||
nextPacketTime = System.nanoTime() + 1000000000L;
|
||||
if (jsonResponse.has("system") && jsonResponse.has("emeter")) {
|
||||
SenseEnergyDatagramListener localPacketListener = packetListener;
|
||||
if (localPacketListener != null) {
|
||||
localPacketListener.requestReceived(packet.getSocketAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendResponse(SocketAddress socketAddress, SenseEnergyDatagramGetSysInfo getSysInfo,
|
||||
SenseEnergyDatagramGetRealtime getRealtime) throws IOException {
|
||||
String jsonResponse = String.format("{\"emeter\":{\"get_realtime\":%s},\"system\":{\"get_sysinfo\":%s}}",
|
||||
gson.toJson(getRealtime), gson.toJson(getSysInfo));
|
||||
|
||||
byte[] encrypted = TpLinkEncryption.encrypt(jsonResponse);
|
||||
|
||||
DatagramSocket localDatagramSocket = datagramSocket;
|
||||
if (localDatagramSocket == null) {
|
||||
logger.warn("sendResponse(): Datagram socket is null, cannot send response");
|
||||
return;
|
||||
}
|
||||
localDatagramSocket.send(new DatagramPacket(encrypted, encrypted.length, socketAddress));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyDatagramListener} Interface for callback when a Sense Energy packet is received
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface SenseEnergyDatagramListener {
|
||||
|
||||
/**
|
||||
* called when a datagram from the Sense Energy monitor is recevied
|
||||
*
|
||||
* @param socketAddress the socket address for the Sense Energy monitor
|
||||
*/
|
||||
void requestReceived(SocketAddress socketAddress);
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.eclipse.jetty.websocket.common.WebSocketSession;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyWebSocketRealtimeUpdate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyWebSocket }
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@WebSocket
|
||||
public class SenseEnergyWebSocket implements WebSocketListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(SenseEnergyWebSocket.class);
|
||||
|
||||
private static final String WSSURL = "wss://clientrt.sense.com/monitors/%s/realtimefeed?access_token=%s";
|
||||
|
||||
private final SenseEnergyWebSocketListener listener;
|
||||
private WebSocketClient client;
|
||||
@Nullable
|
||||
private WebSocketSession session;
|
||||
private boolean closing;
|
||||
private long monitorId;
|
||||
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public boolean isClosing() {
|
||||
return this.closing;
|
||||
}
|
||||
|
||||
public SenseEnergyWebSocket(SenseEnergyWebSocketListener listener, WebSocketClient client) {
|
||||
this.listener = listener;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public void start(long monitorId, String accessToken) throws Exception {
|
||||
logger.debug("Starting Sense Energy WebSocket for monitor ID: {}", monitorId);
|
||||
this.monitorId = monitorId;
|
||||
|
||||
String url = String.format(WSSURL, monitorId, accessToken);
|
||||
session = (WebSocketSession) client.connect(this, new URI(url)).get();
|
||||
}
|
||||
|
||||
public void restart(String accessToken)
|
||||
throws InterruptedException, ExecutionException, IOException, URISyntaxException, Exception {
|
||||
logger.debug("Re-starting Sense Energy WebSocket");
|
||||
|
||||
stop();
|
||||
start(monitorId, accessToken);
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
closing = true;
|
||||
logger.trace("Stopping Sense Energy WebSocket");
|
||||
|
||||
WebSocketSession localSession = session;
|
||||
if (localSession != null) {
|
||||
try {
|
||||
localSession.close();
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error while closing WebSocket session: {}", e.getMessage(), e);
|
||||
} finally {
|
||||
session = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
WebSocketSession localSession = session;
|
||||
return localSession != null && localSession.isRunning();
|
||||
}
|
||||
|
||||
/******* WebSocketListener interface ***********/
|
||||
@Override
|
||||
public void onWebSocketClose(int statusCode, @Nullable String reason) {
|
||||
session = null;
|
||||
logger.trace("WebSocket Close: {} - {}", statusCode, reason);
|
||||
if (!closing) {
|
||||
listener.onWebSocketClose(statusCode, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(@Nullable Session session) {
|
||||
closing = false;
|
||||
logger.debug("Connected to Sense Energy WebSocket");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketError(@Nullable Throwable cause) {
|
||||
String causeMessage = cause != null ? String.valueOf(cause.getMessage()) : "unknown";
|
||||
logger.warn("Sense Energy WebSocket error: {}", causeMessage, cause);
|
||||
|
||||
if (!closing) {
|
||||
// let listener handle restart of socket
|
||||
listener.onWebSocketError(causeMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketBinary(byte @Nullable [] payload, int offset, int len) {
|
||||
logger.warn("Unexpected binary message received");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketText(@Nullable String message) {
|
||||
if (closing || message == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JsonObject jsonResponse = JsonParser.parseString(message).getAsJsonObject();
|
||||
String type = jsonResponse.get("type").getAsString();
|
||||
|
||||
if ("realtime_update".equals(type)) {
|
||||
logger.trace("realtime_update: {}", jsonResponse);
|
||||
SenseEnergyWebSocketRealtimeUpdate update = gson.fromJson(jsonResponse.getAsJsonObject("payload"),
|
||||
SenseEnergyWebSocketRealtimeUpdate.class);
|
||||
if (update != null) {
|
||||
listener.onWebSocketRealtimeUpdate(update);
|
||||
}
|
||||
} else if ("error".equals(type)) {
|
||||
logger.warn("WebSocket error {}", jsonResponse.get("payload").toString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error processing WebSocket message: {}", message, e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyWebSocketRealtimeUpdate;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyWebSocket } interface for callbacks pertaining to the web socket
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface SenseEnergyWebSocketListener {
|
||||
/**
|
||||
* called when the web socket is closed
|
||||
*
|
||||
* @param statusCode
|
||||
* @param reason
|
||||
*/
|
||||
void onWebSocketClose(int statusCode, @Nullable String reason);
|
||||
|
||||
/**
|
||||
* called when there is an error on the web socket
|
||||
*
|
||||
* @param msg
|
||||
*/
|
||||
void onWebSocketError(String msg);
|
||||
|
||||
/**
|
||||
* called with an updated energy usage report
|
||||
*
|
||||
* @param update
|
||||
*/
|
||||
void onWebSocketRealtimeUpdate(SenseEnergyWebSocketRealtimeUpdate update);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyApiAuthenticate}
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyApiAuthenticate {
|
||||
public boolean authorized;
|
||||
@SerializedName("account_id")
|
||||
public long accountID;
|
||||
@SerializedName("user_id")
|
||||
public long userID;
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
public SenseEnergyApiMonitor[] monitors;
|
||||
@SerializedName("bridge_server")
|
||||
public String bridgeServer;
|
||||
@SerializedName("date_created")
|
||||
public Instant dateCreated;
|
||||
@SerializedName("totp_enabled")
|
||||
public transient boolean totpEnabled;
|
||||
@SerializedName("ab_cohort")
|
||||
public transient String abCohort;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyApiDevice } is the dto for the api sense discovered devices
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyApiDevice {
|
||||
public String id;
|
||||
public String name;
|
||||
public String icon;
|
||||
public SenseEnergyApiDeviceTags tags;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyApiDeviceDetection }
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyApiDeviceDetection {
|
||||
@SerializedName("num_detected")
|
||||
public int numDetected;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyApiDeviceTags } is the dto for tag info inside the SenseEnergyApiDevice dto class
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyApiDeviceTags {
|
||||
public enum Stage {
|
||||
Tracking,
|
||||
Inventory
|
||||
}
|
||||
|
||||
@SerializedName("Stage")
|
||||
public Stage stage;
|
||||
@SerializedName("UserDeleted")
|
||||
public boolean userDeleted;
|
||||
@SerializedName("AlwaysOn")
|
||||
public boolean alwaysOn;
|
||||
@SerializedName("DUID")
|
||||
public String deviceID;
|
||||
@SerializedName("SSIEnabled")
|
||||
public boolean ssiEnabled;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyApiGetTrends }
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyApiGetTrends {
|
||||
public int steps;
|
||||
public Instant start;
|
||||
public Instant end;
|
||||
public SenseEnergyApiGetTrendsTotals consumption;
|
||||
public SenseEnergyApiGetTrendsTotals production;
|
||||
@SerializedName("to_grid")
|
||||
public float toGridEnergy;
|
||||
@SerializedName("from_grid")
|
||||
public float fromGridEnergy;
|
||||
@SerializedName("to_grid_cost")
|
||||
public float toGridCost;
|
||||
@SerializedName("form_grid_cost")
|
||||
public float fromGridCost;
|
||||
@SerializedName("solar_powered")
|
||||
public float solarPowered;
|
||||
@SerializedName("net_production")
|
||||
public float netProduction;
|
||||
@SerializedName("production_pct")
|
||||
public float productionPercent;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyApiGetTrendsDevice }
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyApiGetTrendsDevice {
|
||||
public String id;
|
||||
public String name;
|
||||
public String icon;
|
||||
SenseEnergyApiDeviceTags tags;
|
||||
@SerializedName("history")
|
||||
public float[] historyEnergy;
|
||||
@SerializedName("avgw")
|
||||
public float averagePower;
|
||||
@SerializedName("total_kwh")
|
||||
public float totalEnergy;
|
||||
@SerializedName("total_cost")
|
||||
public float totalCost;
|
||||
@SerializedName("pct")
|
||||
public float percent;
|
||||
@SerializedName("cost_history")
|
||||
public float[] historyCost;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyApiGetTrendsTotals }
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyApiGetTrendsTotals {
|
||||
@SerializedName("total")
|
||||
public float totalPower;
|
||||
@SerializedName("totals")
|
||||
public float[] totalsPower;
|
||||
public SenseEnergyApiGetTrendsDevice[] devices;
|
||||
@SerializedName("total_cost")
|
||||
public float totalCost;
|
||||
@SerializedName("total_costs")
|
||||
public float[] totalCosts;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyApiMonitor} MonitorDevice dto structure. All fields are documented here for reference, however
|
||||
* fields
|
||||
* marked as transient are not serialized in order to save processing time.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyApiMonitor {
|
||||
public long id;
|
||||
@SerializedName("date_created")
|
||||
public Instant dateCreated;
|
||||
@SerializedName("time_zone")
|
||||
public String timezone;
|
||||
@SerializedName("solar_connected")
|
||||
public boolean solarConnected;
|
||||
@SerializedName("solar_configured")
|
||||
public boolean solarConfigured;
|
||||
@SerializedName("signal_check_completed_time")
|
||||
public Instant signalCheckCompletedTime;
|
||||
@SerializedName("ethernet_supported")
|
||||
public transient boolean ethernetSupported;
|
||||
@SerializedName("power_over_ethernet_supported")
|
||||
public transient boolean powerOverEthernetSupported;
|
||||
@SerializedName("aux_ignore")
|
||||
public transient boolean auxIgnore;
|
||||
@SerializedName("aux_port")
|
||||
public String auxPort;
|
||||
@SerializedName("hardware_type")
|
||||
public String hardwareType;
|
||||
@SerializedName("zigbee_supported")
|
||||
public transient boolean zigbeeSupported;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyMonitorInfo } is the dto for Monitor info inside the MonitorStatus dto class
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyApiMonitorInfo {
|
||||
public String emac;
|
||||
@SerializedName("wifi_strength")
|
||||
public int wifiStrength;
|
||||
@SerializedName("ip_address")
|
||||
public String ipAddress;
|
||||
public String connection;
|
||||
public String version;
|
||||
public String ssid;
|
||||
public String mac;
|
||||
public boolean ethernet;
|
||||
@SerializedName("test_result")
|
||||
public String testResult;
|
||||
public String serial;
|
||||
@SerializedName("ndt_enabled")
|
||||
public boolean ndtEnabled;
|
||||
public boolean online;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyApiMonitorStatus } is the dto for Monitor status API call
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyApiMonitorStatus {
|
||||
@SerializedName("device_detection")
|
||||
public SenseEnergyApiDeviceDetection deviceDetection;
|
||||
@SerializedName("monitor_info")
|
||||
public SenseEnergyApiMonitorInfo monitorInfo;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyApiRefreshToken }
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class SenseEnergyApiRefreshToken {
|
||||
public boolean authorized;
|
||||
@SerializedName("account_id")
|
||||
public long accountId;
|
||||
@SerializedName("user_id")
|
||||
public long userId;
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
String user;
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
@SerializedName("totp_enabled")
|
||||
public boolean totpEnabled;
|
||||
public Instant expires;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyDatagramGetRealtime } dto
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyDatagramGetRealtime {
|
||||
public float current;
|
||||
public float voltage;
|
||||
public float power;
|
||||
public int total;
|
||||
@SerializedName("err_code")
|
||||
public int errorCode;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyDatagramGetSysInfo } dto for the udp request for sysinfo from sense device
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyDatagramGetSysInfo {
|
||||
@SerializedName("err_code")
|
||||
public int errorCode;
|
||||
@SerializedName("sw_ver")
|
||||
public String swVersion;
|
||||
@SerializedName("hw_ver")
|
||||
public String hwVersion;
|
||||
public String type;
|
||||
public String model;
|
||||
public String mac;
|
||||
public String deviceId;
|
||||
public String alias;
|
||||
@SerializedName("relay_state")
|
||||
public int relayState;
|
||||
public int updating;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyWebSocketDevice} is dto for the websocket messages. Fields which are commented are not used in the
|
||||
* binding, but there for reference.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyWebSocketDevice {
|
||||
public String id;
|
||||
public String name;
|
||||
// public String icon;
|
||||
public float w;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyWebSocketDeviceTags } is the dto for tag info inside the SenseEnergyApiDevice dto class
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyWebSocketDeviceTags {
|
||||
@SerializedName("DUID")
|
||||
public String deviceID;
|
||||
@SerializedName("SSIEnabled")
|
||||
public boolean ssiEnabled;
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyWebSocketRealtimeUpdate } dto object for web socket realtime updates. Fields which are not used
|
||||
* have been commented out in order to save memory and processor bandwidth in the gson conversion.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class SenseEnergyWebSocketRealtimeUpdate {
|
||||
public float[] voltage;
|
||||
// public long frame;
|
||||
public SenseEnergyWebSocketDevice[] devices;
|
||||
// public float defaultCost;
|
||||
// public float[] channels;
|
||||
public float hz;
|
||||
public float w;
|
||||
// public float c;
|
||||
@SerializedName("solar_w")
|
||||
public float solarW;
|
||||
@SerializedName("grid_w")
|
||||
public float gridW;
|
||||
// @SerializedName("solar_c")
|
||||
// public float solarC;
|
||||
// @SerializedName("solar_pct")
|
||||
// public int solarPct;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyBridgeConfiguration}
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyBridgeConfiguration {
|
||||
public String email = "";
|
||||
public String password = "";
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyProxyDeviceConfiguration}
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyProxyDeviceConfiguration {
|
||||
public String macAddress = "";
|
||||
public String powerLevels = "";
|
||||
public float voltage = 120;
|
||||
public String senseName = "";
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.senseenergy.internal.SenseEnergyBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.senseenergy.internal.handler.SenseEnergyBridgeHandler;
|
||||
import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
|
||||
/**
|
||||
* The {@link SenseEnergyDiscoveryService }
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@Component(scope = ServiceScope.PROTOTYPE, service = SenseEnergyDiscoveryService.class, configurationPid = "discovery.senseenergy")
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyDiscoveryService extends AbstractThingHandlerDiscoveryService<SenseEnergyBridgeHandler> {
|
||||
private static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Set.of(MONITOR_THING_TYPE);
|
||||
|
||||
public SenseEnergyDiscoveryService() {
|
||||
super(SenseEnergyBridgeHandler.class, DISCOVERABLE_THING_TYPES_UIDS, 0, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
thingHandler.registerDiscoveryListener(this);
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
thingHandler.unregisterDiscoveryListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
if (thingHandler.getThing().getStatus() == ThingStatus.ONLINE) {
|
||||
thingHandler.refreshMonitors();
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyDiscoveryMonitor(long id) {
|
||||
ThingUID bridgeUID = thingHandler.getThing().getUID();
|
||||
|
||||
ThingUID uid = new ThingUID(MONITOR_THING_TYPE, bridgeUID, String.valueOf(id));
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
|
||||
.withProperty(PARAM_MONITOR_ID, id).withRepresentationProperty(PARAM_MONITOR_ID)
|
||||
.withLabel("Sense Energy Monitor").build();
|
||||
thingDiscovered(result);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.handler;
|
||||
|
||||
import static org.openhab.binding.senseenergy.internal.SenseEnergyBindingConstants.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyApi;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyApiException;
|
||||
import org.openhab.binding.senseenergy.internal.config.SenseEnergyBridgeConfiguration;
|
||||
import org.openhab.binding.senseenergy.internal.discovery.SenseEnergyDiscoveryService;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyBridgeHandler}
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyBridgeHandler extends BaseBridgeHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(SenseEnergyBridgeHandler.class);
|
||||
|
||||
private @Nullable SenseEnergyDiscoveryService discoveryService;
|
||||
|
||||
private SenseEnergyApi api;
|
||||
private SenseEnergyBridgeConfiguration config;
|
||||
|
||||
protected @Nullable ScheduledFuture<?> heartbeatJob;
|
||||
|
||||
private Set<Long> monitorIDs = Collections.emptySet();
|
||||
|
||||
public SenseEnergyBridgeHandler(final Bridge thing, HttpClient httpClient) {
|
||||
super(thing);
|
||||
api = new SenseEnergyApi(httpClient);
|
||||
|
||||
config = getConfigAs(SenseEnergyBridgeConfiguration.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Set.of(SenseEnergyDiscoveryService.class);
|
||||
}
|
||||
|
||||
public SenseEnergyApi getApi() {
|
||||
return api;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
if (config.email.isBlank() || config.password.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"@text/offline.configuration-error.user-credentials-missing");
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
scheduler.execute(this::goOnline);
|
||||
}
|
||||
|
||||
public void goOnline() {
|
||||
try {
|
||||
this.monitorIDs = api.initialize(config.email, config.password);
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) {
|
||||
handleApiException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshMonitors();
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
|
||||
this.heartbeatJob = scheduler.scheduleWithFixedDelay(this::heartbeat, 0, HEARTBEAT_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
private void heartbeat() {
|
||||
ThingStatus thingStatus = getThing().getStatus();
|
||||
|
||||
if (thingStatus == ThingStatus.OFFLINE
|
||||
&& getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.COMMUNICATION_ERROR) {
|
||||
goOnline(); // only attempt to goOnline if not a configuration error
|
||||
return;
|
||||
}
|
||||
|
||||
if (thingStatus != ThingStatus.ONLINE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// token is verified on each api call, called here in case no API calls are made in the alloted period
|
||||
try {
|
||||
getApi().verifyToken();
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) {
|
||||
handleApiException(e);
|
||||
}
|
||||
|
||||
// call heartbeat for each thing to check health
|
||||
getThing().getThings().stream() //
|
||||
.map(t -> t.getHandler()) //
|
||||
.filter(h -> h instanceof SenseEnergyMonitorHandler) //
|
||||
.map(h -> (SenseEnergyMonitorHandler) h) //
|
||||
.forEach(h -> h.heartbeat());
|
||||
}
|
||||
|
||||
public void handleApiException(Exception e) {
|
||||
ThingStatusDetail statusDetail = ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR;
|
||||
|
||||
if (e instanceof SenseEnergyApiException apiException) {
|
||||
statusDetail = apiException.isConfigurationIssue() ? ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR
|
||||
: ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR;
|
||||
} else {
|
||||
logger.debug("Unhandled Exception", e);
|
||||
statusDetail = ThingStatusDetail.OFFLINE.NONE;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, statusDetail, e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
/*
|
||||
* iterate through the monitor IDs (currently initialized when the api initializes) and checks if a thing is
|
||||
* already created. If not, will notify discovery service.
|
||||
*/
|
||||
public void refreshMonitors() {
|
||||
SenseEnergyDiscoveryService localDiscoveryService = discoveryService;
|
||||
if (localDiscoveryService == null) {
|
||||
logger.warn("Discovery service is not initialized. Skipping monitor refresh.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (Long id : monitorIDs) {
|
||||
if (getMonitorHandler(id) == null) {
|
||||
logger.info("Found Sense Energy monitor with ID: {}", id);
|
||||
localDiscoveryService.notifyDiscoveryMonitor(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SenseEnergyMonitorHandler getMonitorHandler(long id) {
|
||||
return getThing().getThings().stream() //
|
||||
.filter(t -> t.getThingTypeUID().equals(MONITOR_THING_TYPE)) //
|
||||
.map(t -> (SenseEnergyMonitorHandler) t.getHandler()) //
|
||||
.filter(Objects::nonNull) //
|
||||
.filter(h -> h.getId() == id) //
|
||||
.findFirst() //
|
||||
.orElse(null); //
|
||||
}
|
||||
|
||||
/*
|
||||
* rediscover the monitors again to add any back to inbox
|
||||
*/
|
||||
@Override
|
||||
public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
|
||||
refreshMonitors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// no channels associated with bridge
|
||||
}
|
||||
|
||||
public boolean registerDiscoveryListener(SenseEnergyDiscoveryService listener) {
|
||||
if (discoveryService == null) {
|
||||
discoveryService = listener;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean unregisterDiscoveryListener() {
|
||||
if (discoveryService != null) {
|
||||
discoveryService = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void dispose() {
|
||||
ScheduledFuture<?> localHeartbeatJob = this.heartbeatJob;
|
||||
if (localHeartbeatJob != null && !localHeartbeatJob.isCancelled()) {
|
||||
localHeartbeatJob.cancel(true);
|
||||
}
|
||||
this.heartbeatJob = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,700 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.handler;
|
||||
|
||||
import static org.openhab.binding.senseenergy.internal.SenseEnergyBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.binding.senseenergy.internal.actions.SenseEnergyMonitorActions;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyApi;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyApiException;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyDatagram;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyDatagramListener;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyWebSocket;
|
||||
import org.openhab.binding.senseenergy.internal.api.SenseEnergyWebSocketListener;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiDevice;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiMonitor;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiMonitorInfo;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyApiMonitorStatus;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyDatagramGetRealtime;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyDatagramGetSysInfo;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyWebSocketDevice;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyWebSocketRealtimeUpdate;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelGroupUID;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingStatusInfo;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.thing.type.ChannelDefinition;
|
||||
import org.openhab.core.thing.type.ChannelGroupType;
|
||||
import org.openhab.core.thing.type.ChannelGroupTypeRegistry;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeRegistry;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyBridgeHandler} is the handler for Sense API and connects it
|
||||
* to the webservice.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyMonitorHandler extends BaseBridgeHandler
|
||||
implements SenseEnergyWebSocketListener, SenseEnergyDatagramListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(SenseEnergyMonitorHandler.class);
|
||||
|
||||
private static final int MAX_RESPONSES_PER_REQUEST = 5;
|
||||
private static final int SENSE_DATAGRAM_BCAST_PORT = 9999;
|
||||
|
||||
private static final String CHANNEL_PROPERTY_ID = "sense-id";
|
||||
private static final String CHANNEL_PROPERTY_LABEL = "sense-label";
|
||||
|
||||
private long id;
|
||||
@Nullable
|
||||
private SenseEnergyApiMonitorStatus apiMonitorStatus;
|
||||
|
||||
private SenseEnergyWebSocket webSocket;
|
||||
private SenseEnergyDatagram datagram;
|
||||
|
||||
private final ChannelGroupTypeRegistry channelGroupTypeRegistry;
|
||||
private final ChannelTypeRegistry channelTypeRegistry;
|
||||
|
||||
public enum DeviceType {
|
||||
DISCOVERED_DEVICE,
|
||||
SELF_REPORTING_DEVICE,
|
||||
PROXY_DEVICE;
|
||||
|
||||
public static String getChannelGroup(DeviceType deviceType) {
|
||||
return switch (deviceType) {
|
||||
case PROXY_DEVICE -> CHANNEL_GROUP_PROXY_DEVICES;
|
||||
case SELF_REPORTING_DEVICE -> CHANNEL_GROUP_SELF_REPORTING_DEVICES;
|
||||
default -> CHANNEL_GROUP_DISCOVERED_DEVICES;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Map of all device types from the api
|
||||
private Map<String, SenseEnergyApiDevice> senseDevices = Collections.emptyMap();
|
||||
// DeviceTypes deduced from the senseDevices
|
||||
private Map<String, DeviceType> senseDevicesType = new HashMap<String, DeviceType>();
|
||||
// Keep track of which devices are on so we can send trigger when devices are turned on/off
|
||||
private Set<String> devicesOn = Collections.emptySet();
|
||||
|
||||
private static final Set<String> GENERATED_CHANNEL_GROUPS = Set.of(CHANNEL_GROUP_DISCOVERED_DEVICES,
|
||||
CHANNEL_GROUP_SELF_REPORTING_DEVICES, CHANNEL_GROUP_PROXY_DEVICES);
|
||||
|
||||
// counter to slow down updates to openHAB for every power update
|
||||
private int countRealTimeUpdate;
|
||||
|
||||
private Iterator<Thing> roundRobinIterator = Collections.emptyIterator();
|
||||
|
||||
boolean solarConfigured = true;
|
||||
|
||||
public SenseEnergyMonitorHandler(final Bridge thing, final WebSocketClient webSocketClient,
|
||||
final ChannelGroupTypeRegistry channelGroupTypeRegistry, final ChannelTypeRegistry channelTypeRegistry) {
|
||||
super(thing);
|
||||
|
||||
this.channelGroupTypeRegistry = channelGroupTypeRegistry;
|
||||
this.channelTypeRegistry = channelTypeRegistry;
|
||||
|
||||
this.webSocket = new SenseEnergyWebSocket(this, webSocketClient);
|
||||
this.datagram = new SenseEnergyDatagram(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return List.of(SenseEnergyMonitorActions.class);
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
id = ((Number) getThing().getConfiguration().get(PARAM_MONITOR_ID)).intValue();
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
scheduler.execute(this::goOnline);
|
||||
}
|
||||
|
||||
public void goOnline() {
|
||||
if (getThing().getStatus() == ThingStatus.ONLINE || !checkBridgeStatus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SenseEnergyApiMonitor apiMonitor = getApi().getMonitorOverview(id);
|
||||
this.solarConfigured = apiMonitor.solarConfigured;
|
||||
apiMonitorStatus = getApi().getMonitorStatus(id);
|
||||
refreshDevices();
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) {
|
||||
handleApiException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
ThingBuilder thingBuilder = editThing();
|
||||
ThingUID thingUID = getThing().getUID();
|
||||
|
||||
if (solarConfigured) {
|
||||
thingBuilder.withoutChannel(new ChannelUID(thingUID, CHANNEL_GROUP_GENERAL, CHANNEL_LEG_1_POWER));
|
||||
thingBuilder.withoutChannel(new ChannelUID(thingUID, CHANNEL_GROUP_GENERAL, CHANNEL_LEG_2_POWER));
|
||||
} else {
|
||||
thingBuilder.withoutChannel(new ChannelUID(thingUID, CHANNEL_GROUP_GENERAL, CHANNEL_MAIN_POWER));
|
||||
thingBuilder.withoutChannel(new ChannelUID(thingUID, CHANNEL_GROUP_GENERAL, CHANNEL_SOLAR_POWER));
|
||||
}
|
||||
|
||||
reconcileDiscoveredDeviceChannels(thingBuilder);
|
||||
updateThing(thingBuilder.build());
|
||||
updateProperties();
|
||||
|
||||
try {
|
||||
webSocket.start(id, getApi().getAccessToken());
|
||||
} catch (Exception e) {
|
||||
handleApiException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
public void heartbeat() {
|
||||
ThingStatus thingStatus = getThing().getStatus();
|
||||
|
||||
if (thingStatus == ThingStatus.OFFLINE
|
||||
&& getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.COMMUNICATION_ERROR) {
|
||||
goOnline(); // only attempt to goOnline if not a configuration error
|
||||
return;
|
||||
}
|
||||
|
||||
if (thingStatus != ThingStatus.ONLINE) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.trace("SenseEnergyMonitorHandler: heartbeat");
|
||||
refreshDevices();
|
||||
reconcileDiscoveredDeviceChannels(null);
|
||||
|
||||
if (!webSocket.isRunning()) {
|
||||
logger.debug("heartbeat: webSocket not running");
|
||||
try {
|
||||
webSocket.restart(getApi().getAccessToken());
|
||||
} catch (Exception e) {
|
||||
handleApiException(e);
|
||||
}
|
||||
}
|
||||
|
||||
checkDatagramStatus();
|
||||
}
|
||||
|
||||
public void handleApiException(Exception e) {
|
||||
ThingStatusDetail statusDetail = ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR;
|
||||
|
||||
if (e instanceof SenseEnergyApiException apiException) {
|
||||
statusDetail = apiException.isConfigurationIssue() ? ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR
|
||||
: ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR;
|
||||
} else {
|
||||
logger.debug("Unhandled Exception", e);
|
||||
statusDetail = ThingStatusDetail.OFFLINE.NONE;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, statusDetail, e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
webSocket.stop();
|
||||
datagram.stop();
|
||||
}
|
||||
|
||||
public void updateProperties() {
|
||||
updateProperty(PROPERTY_MONITOR_SOLAR_CONFIGURED, Boolean.toString(solarConfigured));
|
||||
|
||||
SenseEnergyApiMonitorStatus localMonitorStatus = apiMonitorStatus;
|
||||
if (localMonitorStatus != null && localMonitorStatus.monitorInfo != null) {
|
||||
SenseEnergyApiMonitorInfo info = Objects.requireNonNull(localMonitorStatus.monitorInfo);
|
||||
updateProperty(PROPERTY_MONITOR_IP_ADDRESS, Objects.requireNonNullElse(info.ipAddress, ""));
|
||||
updateProperty(PROPERTY_MONITOR_VERSION, Objects.requireNonNullElse(info.version, ""));
|
||||
updateProperty(PROPERTY_MONITOR_SERIAL, Objects.requireNonNullElse(info.serial, ""));
|
||||
updateProperty(PROPERTY_MONITOR_SSID, Objects.requireNonNullElse(info.ssid, ""));
|
||||
updateProperty(PROPERTY_MONITOR_MAC, Objects.requireNonNullElse(info.mac, ""));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
} else if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
|
||||
goOnline();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkBridgeStatus() {
|
||||
Bridge bridge = this.getBridge();
|
||||
if (bridge == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.configuration-error.bridge-missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
SenseEnergyBridgeHandler bridgeHandler = (SenseEnergyBridgeHandler) bridge.getHandler();
|
||||
if (bridgeHandler == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bridgeHandler.getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
SenseEnergyBridgeHandler getBridgeHandler() {
|
||||
Bridge bridge = getBridge();
|
||||
return (bridge != null) ? (SenseEnergyBridgeHandler) bridge.getHandler() : null;
|
||||
}
|
||||
|
||||
public SenseEnergyApi getApi() {
|
||||
SenseEnergyBridgeHandler handler = Objects.requireNonNull(getBridgeHandler(),
|
||||
"Invalid state where handler is null");
|
||||
return handler.getApi();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
String channelGroup = channelUID.getGroupId();
|
||||
if (channelGroup == null) {
|
||||
logger.debug("Channel does not have a group ID: {}", channelUID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (GENERATED_CHANNEL_GROUPS.contains(channelGroup)) {
|
||||
Channel channel = getThing().getChannel(channelUID);
|
||||
if (channel == null) {
|
||||
logger.debug("Channel does not exist: {}", channelUID);
|
||||
return;
|
||||
}
|
||||
|
||||
String senseID = channel.getProperties().get(CHANNEL_PROPERTY_ID);
|
||||
if (senseID == null) {
|
||||
logger.debug("Channel does not have a senseID property: {}", channelUID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!devicesOn.contains(senseID)) {
|
||||
updateState(channelUID, new QuantityType<>(0, Units.WATT));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduces the device type based by examining the properties and identifying if the device is a proxy device
|
||||
* in openHAB.
|
||||
*
|
||||
* @param apiDevice The device for which the type needs to be deduced.
|
||||
* @return The deduced DeviceType.
|
||||
*/
|
||||
private DeviceType deduceDeviceType(SenseEnergyApiDevice apiDevice) {
|
||||
if (!apiDevice.tags.ssiEnabled) {
|
||||
return DeviceType.DISCOVERED_DEVICE;
|
||||
}
|
||||
|
||||
SenseEnergyProxyDeviceHandler proxyHandler = getProxyDeviceByMAC(apiDevice.tags.deviceID);
|
||||
return (proxyHandler != null) ? DeviceType.PROXY_DEVICE : DeviceType.SELF_REPORTING_DEVICE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the list of devices by retrieving them from the API and then updating the map of DeviceTypes.
|
||||
*/
|
||||
private void refreshDevices() {
|
||||
try {
|
||||
senseDevices = getApi().getDevices(id);
|
||||
|
||||
senseDevices.entrySet().stream() //
|
||||
.filter(e -> !senseDevicesType.containsKey(e.getKey())) //
|
||||
.forEach(e -> senseDevicesType.put(e.getKey(), deduceDeviceType(e.getValue())));
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | SenseEnergyApiException e) {
|
||||
handleApiException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Reconciles the discovered device channels to stay in sync with the discovered device list to ensure
|
||||
* all the channels in the channel template exist for every sense devices.
|
||||
*
|
||||
* @param thingBuilder to update if already editing, otherwise will open
|
||||
*/
|
||||
public void reconcileDiscoveredDeviceChannels(@Nullable ThingBuilder thingBuilder) {
|
||||
ChannelGroupType channelGroupType = Objects
|
||||
.requireNonNull(channelGroupTypeRegistry.getChannelGroupType(CHANNEL_GROUP_TYPE_DEVICE_TEMPLATE));
|
||||
List<ChannelDefinition> channelDefinitions = channelGroupType.getChannelDefinitions();
|
||||
|
||||
Set<String> senseIDs = new HashSet<>(senseDevices.keySet());
|
||||
senseIDs.remove("solar"); // don't create solar as a separate channel
|
||||
|
||||
logger.trace("Reconciling channels with Sense device, channel count: {}", senseIDs.size());
|
||||
|
||||
boolean channelsUpdated = false;
|
||||
ThingBuilder localBuilder = (thingBuilder != null) ? thingBuilder : editThing();
|
||||
|
||||
// reconcile every channel type that is in the group template
|
||||
for (ChannelDefinition channelDefinition : channelDefinitions) {
|
||||
ChannelType channelType = Objects
|
||||
.requireNonNull(channelTypeRegistry.getChannelType(channelDefinition.getChannelTypeUID()));
|
||||
// create set of IDs of existing channels of the TypeUID from the template.
|
||||
@SuppressWarnings("null")
|
||||
Set<String> existingChannels = getThing().getChannels().stream() //
|
||||
.filter(ch -> GENERATED_CHANNEL_GROUPS.contains(ch.getUID().getGroupId())) //
|
||||
.filter(ch -> ch.getChannelTypeUID() != null && ch.getChannelTypeUID().equals(channelType.getUID())) //
|
||||
.map(ch -> ch.getProperties().get(CHANNEL_PROPERTY_ID)) //
|
||||
.filter(Objects::nonNull) //
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Set<String> allChannels = new HashSet<>(existingChannels);
|
||||
allChannels.addAll(senseIDs);
|
||||
|
||||
for (String senseID : allChannels) {
|
||||
DeviceType deviceType = senseDevicesType.getOrDefault(senseID, DeviceType.DISCOVERED_DEVICE);
|
||||
|
||||
ChannelUID channelUID = makeDeviceChannelUID(deviceType, senseID, channelDefinition.getId());
|
||||
if (existingChannels.contains(senseID) && !senseIDs.contains(senseID)) { // remove outdated channel
|
||||
localBuilder.withoutChannel(channelUID);
|
||||
channelsUpdated = true;
|
||||
} else if (existingChannels.contains(senseID) && senseIDs.contains(senseID)) { // update existing
|
||||
String deviceName = Objects.requireNonNull(senseDevices.get(senseID)).name;
|
||||
channelsUpdated |= updateGeneratedChannel(localBuilder, channelUID, deviceName);
|
||||
} else if (!existingChannels.contains(senseID) && senseIDs.contains(senseID)) { // add new channel
|
||||
addGeneratedChannel(localBuilder, channelUID, Objects.requireNonNull(senseDevices.get(senseID)),
|
||||
channelDefinition, channelType);
|
||||
channelsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (thingBuilder == null) {
|
||||
updateThing(localBuilder.build());
|
||||
}
|
||||
|
||||
if (channelsUpdated) {
|
||||
triggerChannel(new ChannelUID(getThing().getUID(), CHANNEL_GROUP_GENERAL, CHANNEL_DEVICES_UPDATED_TRIGGER));
|
||||
}
|
||||
}
|
||||
|
||||
public void addGeneratedChannel(ThingBuilder builder, ChannelUID channelUID, SenseEnergyApiDevice apiDevice,
|
||||
ChannelDefinition channelDefinition, ChannelType channelType) {
|
||||
Channel channel = ChannelBuilder.create(channelUID)
|
||||
.withDescription(Objects.requireNonNull(channelDefinition.getDescription())) //
|
||||
.withLabel(apiDevice.name + ": " + channelDefinition.getLabel()) //
|
||||
.withProperties(Map.of(CHANNEL_PROPERTY_ID, apiDevice.id, CHANNEL_PROPERTY_LABEL, apiDevice.name)) //
|
||||
.withAcceptedItemType(channelType.getItemType()) //
|
||||
.withType(channelDefinition.getChannelTypeUID()) //
|
||||
.withKind(channelType.getKind()) //
|
||||
.withDefaultTags(channelType.getTags()) //
|
||||
.build();
|
||||
builder.withChannel(channel);
|
||||
}
|
||||
|
||||
/*
|
||||
* updates channel label of an existing channel by removing it and then added it back
|
||||
*
|
||||
* @param thingBuilder
|
||||
*
|
||||
* @param channelUID
|
||||
*
|
||||
* @param label
|
||||
*
|
||||
* @return whether channel needed to be updated
|
||||
*/
|
||||
public boolean updateGeneratedChannel(ThingBuilder thingBuilder, ChannelUID channelUID, String label) {
|
||||
Channel channel = getThing().getChannel(channelUID);
|
||||
|
||||
if (channel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<String, String> properties = channel.getProperties();
|
||||
|
||||
String currentLabel = properties.get(CHANNEL_PROPERTY_LABEL);
|
||||
if (label.equals(currentLabel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
thingBuilder.withoutChannel(channelUID);
|
||||
|
||||
Map<String, String> newProperties = new HashMap<String, String>(properties);
|
||||
newProperties.put(CHANNEL_PROPERTY_LABEL, label);
|
||||
|
||||
Channel newChannel = ChannelBuilder.create(channel).withProperties(newProperties).withLabel(label).build();
|
||||
|
||||
thingBuilder.withChannel(newChannel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* create a channelUID in the designated/consistent format
|
||||
*
|
||||
* @param senseID
|
||||
*
|
||||
* @param channelID
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public ChannelUID makeDeviceChannelUID(DeviceType deviceType, String senseID, String channelID) {
|
||||
ChannelGroupUID channelGroupUID = new ChannelGroupUID(getThing().getUID(),
|
||||
DeviceType.getChannelGroup(deviceType));
|
||||
|
||||
return new ChannelUID(channelGroupUID, senseID + "-" + channelID);
|
||||
}
|
||||
|
||||
/*
|
||||
* helper function to update channel state
|
||||
*
|
||||
* @param channelGroup
|
||||
*
|
||||
* @param channel
|
||||
*
|
||||
* @param value
|
||||
*
|
||||
* @param unit
|
||||
*/
|
||||
public void updateChannel(String channelGroup, String channel, float value, Unit<?> unit) {
|
||||
ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelGroup, channel);
|
||||
updateState(channelUID, new QuantityType<>(value, unit));
|
||||
}
|
||||
|
||||
/*
|
||||
* start/stop datagram when a proxy device is ONLINE or when they all are OFFLINE
|
||||
*
|
||||
* @param proxyDeviceHandler
|
||||
*
|
||||
* @param thingStatus
|
||||
*/
|
||||
public void childStatusChange(SenseEnergyProxyDeviceHandler proxyDeviceHandler, ThingStatus thingStatus) {
|
||||
if (thingStatus == ThingStatus.ONLINE && getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
throw new IllegalStateException("Child should never go ONLINE w/o the bridge being online");
|
||||
}
|
||||
|
||||
checkDatagramStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* start/stop and check datagram status depending on whether there are proxy devices or not
|
||||
*/
|
||||
public void checkDatagramStatus() {
|
||||
final String datagramListenerThreadName = "OH-binding-" + getThing().getUID().getAsString();
|
||||
|
||||
boolean childOnline = getThing().getThings().stream() //
|
||||
.filter(t -> t.getThingTypeUID().equals(PROXY_DEVICE_THING_TYPE) && t.getStatus() == ThingStatus.ONLINE) //
|
||||
.findAny() //
|
||||
.isPresent();
|
||||
|
||||
if (childOnline && !datagram.isRunning()) {
|
||||
datagram.stop();
|
||||
try {
|
||||
datagram.start(SENSE_DATAGRAM_BCAST_PORT, datagramListenerThreadName);
|
||||
} catch (IOException e) {
|
||||
handleApiException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!childOnline && datagram.isRunning()) {
|
||||
datagram.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* forms and sends response on request for update on power for proxy device
|
||||
*
|
||||
* @param handler of proxy device
|
||||
*
|
||||
* @param socketAddress of Sense monitor where power update should be sent
|
||||
*/
|
||||
public boolean sendResponse(SenseEnergyProxyDeviceHandler handler, SocketAddress socketAddress) {
|
||||
SenseEnergyDatagramGetSysInfo getSysInfo = new SenseEnergyDatagramGetSysInfo();
|
||||
SenseEnergyDatagramGetRealtime getRealtime = new SenseEnergyDatagramGetRealtime();
|
||||
|
||||
boolean shouldRespond = handler.formPowerResponse(getSysInfo, getRealtime);
|
||||
|
||||
if (!shouldRespond) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.datagram.sendResponse(socketAddress, getSysInfo, getRealtime);
|
||||
} catch (IOException e) {
|
||||
logger.debug("Unable to send datagram response: {}", e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**** Datagram listener functions *****/
|
||||
|
||||
/*
|
||||
* handles request for responding to power queries from Sense device. Will limit the number of responses per request
|
||||
* so as not to overload the Sense monitor. Uses a round robin to ensure all devices have equal opportunity to
|
||||
* respond.
|
||||
*/
|
||||
@Override
|
||||
public void requestReceived(SocketAddress socketAddress) {
|
||||
int maxResponses = MAX_RESPONSES_PER_REQUEST;
|
||||
|
||||
// restart from beginning if RR has reached end
|
||||
if (!roundRobinIterator.hasNext() && !getThing().getThings().isEmpty()) {
|
||||
roundRobinIterator = getThing().getThings().iterator();
|
||||
}
|
||||
|
||||
while (roundRobinIterator.hasNext() && maxResponses > 0) {
|
||||
if (roundRobinIterator.next().getHandler() instanceof SenseEnergyProxyDeviceHandler handler) {
|
||||
if (sendResponse(handler, socketAddress)) {
|
||||
maxResponses--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***** SenseEnergyeWSListener interfaces *****/
|
||||
|
||||
@Override
|
||||
public void onWebSocketClose(int statusCode, @Nullable String reason) {
|
||||
logger.debug("onWebSocketClose ({}), {}", statusCode, reason);
|
||||
// will restart on heartbeat
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketError(String msg) {
|
||||
// no action - let heartbeat restart webSocket
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketRealtimeUpdate(SenseEnergyWebSocketRealtimeUpdate update) {
|
||||
// message comes in MANY times a second, reduce frequency to be good citizen in openHAB
|
||||
countRealTimeUpdate = (countRealTimeUpdate == 0) ? 10 : countRealTimeUpdate - 1;
|
||||
if (countRealTimeUpdate != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (solarConfigured) {
|
||||
updateChannel(CHANNEL_GROUP_GENERAL, CHANNEL_POTENTIAL_1, update.voltage[0], Units.VOLT);
|
||||
updateChannel(CHANNEL_GROUP_GENERAL, CHANNEL_POTENTIAL_2, update.voltage[1], Units.VOLT);
|
||||
|
||||
updateChannel(CHANNEL_GROUP_GENERAL, CHANNEL_MAIN_POWER, update.w, Units.WATT);
|
||||
updateChannel(CHANNEL_GROUP_GENERAL, CHANNEL_SOLAR_POWER, update.solarW, Units.WATT);
|
||||
|
||||
updateChannel(CHANNEL_GROUP_GENERAL, CHANNEL_FREQUENCY, update.hz, Units.HERTZ);
|
||||
updateChannel(CHANNEL_GROUP_GENERAL, CHANNEL_GRID_POWER, update.gridW, Units.WATT);
|
||||
} else {
|
||||
logger.warn("Non solar configured monitors are not currently supported");
|
||||
updateChannel(CHANNEL_GROUP_GENERAL, CHANNEL_POTENTIAL_1, update.voltage[0], Units.VOLT);
|
||||
updateChannel(CHANNEL_GROUP_GENERAL, CHANNEL_POTENTIAL_2, update.voltage[1], Units.VOLT);
|
||||
|
||||
updateChannel(CHANNEL_GROUP_GENERAL, CHANNEL_FREQUENCY, update.hz, Units.HERTZ);
|
||||
}
|
||||
|
||||
updateDiscoveredDevicesStatus(update.devices);
|
||||
}
|
||||
|
||||
/*
|
||||
* updates the power state for discovered devices. sends trigger for devices which turn on/off. if necessary,
|
||||
* reconciles the channels if there are new discovered devices
|
||||
*
|
||||
* @param devices array retrieved in websocket update
|
||||
*/
|
||||
public void updateDiscoveredDevicesStatus(SenseEnergyWebSocketDevice[] devices) {
|
||||
Set<String> updateDevicesOn = new HashSet<>();
|
||||
|
||||
for (SenseEnergyWebSocketDevice device : devices) {
|
||||
// include in the "ON' devices - must be done before reconcileDeviceChannels to prevent recursive loop
|
||||
updateDevicesOn.add(device.id);
|
||||
|
||||
// check if device channels need to be updated because there is a new device
|
||||
if (!senseDevices.containsKey(device.id)) {
|
||||
reconcileDiscoveredDeviceChannels(null);
|
||||
}
|
||||
|
||||
DeviceType deviceType = senseDevicesType.getOrDefault(device.id, DeviceType.DISCOVERED_DEVICE);
|
||||
|
||||
// Send trigger if device just turned on
|
||||
if (!this.devicesOn.contains(device.id)) {
|
||||
triggerChannel(makeDeviceChannelUID(deviceType, device.id, CHANNEL_DEVICE_TRIGGER), "ON");
|
||||
logger.trace("Discovered device turned on: {}({})", device.name, device.id);
|
||||
}
|
||||
|
||||
ChannelUID channelUID = makeDeviceChannelUID(deviceType, device.id, CHANNEL_DEVICE_POWER);
|
||||
if (isLinked(channelUID)) {
|
||||
updateState(channelUID, new QuantityType<>(device.w, Units.WATT));
|
||||
}
|
||||
}
|
||||
|
||||
// if was ON before and not ON now, update state to 0 and send trigger
|
||||
Set<String> wasOnNowOff = new HashSet<>(this.devicesOn);
|
||||
wasOnNowOff.removeAll(updateDevicesOn);
|
||||
for (String id : wasOnNowOff) {
|
||||
DeviceType deviceType = senseDevicesType.getOrDefault(id, DeviceType.DISCOVERED_DEVICE);
|
||||
updateState(makeDeviceChannelUID(deviceType, id, CHANNEL_DEVICE_POWER), new QuantityType<>(0, Units.WATT));
|
||||
triggerChannel(makeDeviceChannelUID(deviceType, id, CHANNEL_DEVICE_TRIGGER), "OFF");
|
||||
logger.trace("Discovered device turned off: {}", id);
|
||||
}
|
||||
this.devicesOn = updateDevicesOn;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SenseEnergyProxyDeviceHandler getProxyDeviceByMAC(String macAddress) {
|
||||
return getThing().getThings().stream() //
|
||||
.filter(t -> t.getThingTypeUID().equals(PROXY_DEVICE_THING_TYPE)) //
|
||||
.map(t -> (SenseEnergyProxyDeviceHandler) t.getHandler()) //
|
||||
.filter(Objects::nonNull) //
|
||||
.filter(h -> h.getMAC().equals(macAddress)) //
|
||||
.findFirst() //
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.handler;
|
||||
|
||||
import static org.openhab.binding.senseenergy.internal.SenseEnergyBindingConstants.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyDatagramGetRealtime;
|
||||
import org.openhab.binding.senseenergy.internal.api.dto.SenseEnergyDatagramGetSysInfo;
|
||||
import org.openhab.binding.senseenergy.internal.config.SenseEnergyProxyDeviceConfiguration;
|
||||
import org.openhab.binding.senseenergy.internal.handler.helpers.SenseEnergyPowerLevels;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* @link { SenseEnergyProxyDeviceHandler }
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyProxyDeviceHandler extends BaseThingHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(SenseEnergyProxyDeviceHandler.class);
|
||||
|
||||
private static final byte[] OUI = new byte[] { 0x53, 0x75, 0x31 };
|
||||
private static final String PROXY_DEVICE_SW_VERSION = "1.2.5 Build 171206 Rel.085954";
|
||||
private static final String PROXY_DEVICE_HW_VERSION = "1.0";
|
||||
private static final String PROXY_DEVICE_TYPE = "IOT.SMARTPLUGSWITCH";
|
||||
private static final String PROXY_DEVICE_MODEL = "HS110(US)";
|
||||
|
||||
private SenseEnergyProxyDeviceConfiguration config = new SenseEnergyProxyDeviceConfiguration();
|
||||
|
||||
SenseEnergyPowerLevels powerLevels = new SenseEnergyPowerLevels();
|
||||
|
||||
private ElectricalData electricalData = new ElectricalData();
|
||||
private boolean selfConfigurationChange = false;
|
||||
|
||||
public SenseEnergyProxyDeviceHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(SenseEnergyProxyDeviceConfiguration.class);
|
||||
|
||||
electricalData.setVoltage(config.voltage);
|
||||
|
||||
Configuration c = null;
|
||||
if (config.macAddress.isBlank()) {
|
||||
byte[] mac = randomizeMAC(OUI);
|
||||
|
||||
String macAddress = HexFormat.of().withDelimiter(":").formatHex(mac).toUpperCase();
|
||||
logger.debug("Spoof MAC address: {}", macAddress);
|
||||
|
||||
selfConfigurationChange = true;
|
||||
c = this.editConfiguration();
|
||||
c.put(CONFIG_PARAMETER_MAC, macAddress);
|
||||
config.macAddress = macAddress;
|
||||
}
|
||||
|
||||
if (!config.powerLevels.isBlank()) {
|
||||
try {
|
||||
powerLevels.parse(config.powerLevels);
|
||||
} catch (IllegalArgumentException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid power level entry");
|
||||
return;
|
||||
}
|
||||
String pretty = powerLevels.toString();
|
||||
if (!pretty.equals(config.powerLevels)) {
|
||||
selfConfigurationChange = true;
|
||||
if (c == null) {
|
||||
c = this.editConfiguration();
|
||||
}
|
||||
|
||||
c.put(CONFIG_PARAMETER_POWER_LEVELS, pretty);
|
||||
}
|
||||
}
|
||||
|
||||
if (c != null) {
|
||||
this.updateConfiguration(c);
|
||||
}
|
||||
|
||||
goOnline();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(ThingStatus thingStatus) {
|
||||
this.updateStatus(thingStatus, ThingStatusDetail.NONE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(ThingStatus thingStatus, ThingStatusDetail thingStatusDetail) {
|
||||
this.updateStatus(thingStatus, thingStatusDetail, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(ThingStatus thingStatus, ThingStatusDetail thingStatusDetail,
|
||||
@Nullable String description) {
|
||||
super.updateStatus(thingStatus, thingStatusDetail, description);
|
||||
|
||||
if (getBridge() instanceof Bridge bridge) {
|
||||
if (bridge.getHandler() instanceof SenseEnergyMonitorHandler monitorHandler) {
|
||||
monitorHandler.childStatusChange(this, thingStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
|
||||
// prevent re-initialization when handler changed configuration
|
||||
if (this.selfConfigurationChange) {
|
||||
this.selfConfigurationChange = false;
|
||||
return;
|
||||
}
|
||||
super.handleConfigurationUpdate(configurationParameters);
|
||||
}
|
||||
|
||||
public void goOnline() {
|
||||
if (!checkBridgeStatus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("Received command");
|
||||
if (command instanceof RefreshType) {
|
||||
// these are input only channels
|
||||
return;
|
||||
}
|
||||
|
||||
QuantityType<Power> qt;
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_PROXY_DEVICE_POWER:
|
||||
if (command instanceof QuantityType<?> qtAbs) {
|
||||
if (qtAbs.getUnit().isCompatible(Units.WATT)) {
|
||||
// guaranteed to be compatible with Units.WATT
|
||||
electricalData.setPower(Objects.requireNonNull(qtAbs.toUnit(Units.WATT)).floatValue());
|
||||
logger.debug("Received power update: {} -> {}", command.toString(), electricalData.getPower());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case CHANNEL_PROXY_DEVICE_SWITCH:
|
||||
case CHANNEL_PROXY_DEVICE_DIMMER:
|
||||
if (command instanceof OnOffType onOffCommand) {
|
||||
qt = powerLevels.getLevel((onOffCommand == OnOffType.ON) ? 100 : 0);
|
||||
if (qt != null) {
|
||||
electricalData.setPower(qt.floatValue());
|
||||
logger.debug("Received switch update: {} -> {}", command.toString(), qt);
|
||||
return;
|
||||
} else {
|
||||
qt = powerLevels.getLevel((onOffCommand == OnOffType.ON) ? "ON" : "OFF");
|
||||
if (qt != null) {
|
||||
electricalData.setPower(qt.floatValue());
|
||||
logger.debug("Received switch update: {} -> {}", command.toString(), qt);
|
||||
return;
|
||||
}
|
||||
logger.debug("No power levels specified for command: {}", command);
|
||||
}
|
||||
} else if (command instanceof PercentType percentType) {
|
||||
qt = powerLevels.getLevel(percentType.intValue());
|
||||
if (qt != null) {
|
||||
electricalData.setPower(qt.floatValue());
|
||||
logger.debug("Received dimmer update: {} -> {}", command.toString(), qt);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CHANNEL_PROXY_DEVICE_STATE: {
|
||||
if (command instanceof StringType stringCommand) {
|
||||
qt = powerLevels.getLevel(stringCommand.toString());
|
||||
if (qt != null) {
|
||||
electricalData.setPower(qt.floatValue());
|
||||
logger.debug("Received state update: {} -> {}", command.toString(), qt);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
} else if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
|
||||
goOnline();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkBridgeStatus() {
|
||||
Bridge bridge = this.getBridge();
|
||||
if (bridge == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.configuration-error.bridge-missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
SenseEnergyMonitorHandler bridgeHandler = (SenseEnergyMonitorHandler) bridge.getHandler();
|
||||
if (bridgeHandler == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bridgeHandler.getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean formPowerResponse(SenseEnergyDatagramGetSysInfo getSysInfo,
|
||||
SenseEnergyDatagramGetRealtime getRealtime) {
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
getSysInfo.swVersion = PROXY_DEVICE_SW_VERSION;
|
||||
getSysInfo.hwVersion = PROXY_DEVICE_HW_VERSION;
|
||||
getSysInfo.type = PROXY_DEVICE_TYPE;
|
||||
getSysInfo.model = PROXY_DEVICE_MODEL;
|
||||
getSysInfo.relayState = 1; // not sure what this does
|
||||
|
||||
getSysInfo.mac = getMAC();
|
||||
getSysInfo.deviceId = getThing().getUID().getId().toString();
|
||||
getSysInfo.alias = (config.senseName.isBlank()) ? getThing().getLabel() : config.senseName;
|
||||
getSysInfo.errorCode = 0;
|
||||
|
||||
getRealtime.current = Math.round(electricalData.getCurrent() * 10) / 10;
|
||||
getRealtime.voltage = Math.round(electricalData.getVoltage());
|
||||
getRealtime.power = Math.round(electricalData.getPower() * 10) / 10;
|
||||
getRealtime.errorCode = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public ElectricalData getElectricalData() {
|
||||
return this.electricalData;
|
||||
}
|
||||
|
||||
public String getMAC() {
|
||||
return config.macAddress;
|
||||
}
|
||||
|
||||
public byte[] randomizeMAC(byte @Nullable [] oui) {
|
||||
byte[] macAddress = new byte[6];
|
||||
Random rand = new Random();
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
macAddress[i] = (byte) rand.nextInt(255);
|
||||
}
|
||||
|
||||
if (oui != null) {
|
||||
macAddress = Arrays.copyOf(oui, oui.length);
|
||||
}
|
||||
|
||||
return macAddress;
|
||||
}
|
||||
|
||||
class ElectricalData {
|
||||
private float power;
|
||||
private float voltage;
|
||||
private float current;
|
||||
|
||||
public void setVoltage(float voltage) {
|
||||
this.voltage = voltage;
|
||||
}
|
||||
|
||||
public void setCurrent(float current) {
|
||||
this.current = current;
|
||||
this.power = voltage * current;
|
||||
}
|
||||
|
||||
public void setPower(float power) {
|
||||
this.power = power;
|
||||
this.current = (voltage != 0) ? power / voltage : 0;
|
||||
}
|
||||
|
||||
public float getVoltage() {
|
||||
return voltage;
|
||||
}
|
||||
|
||||
public float getCurrent() {
|
||||
return current;
|
||||
}
|
||||
|
||||
public float getPower() {
|
||||
return power;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.handler.helpers;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
|
||||
/**
|
||||
* @link { SenseEnergyPowerLevels } is a helper for the @link { SenseEnergyProxyDeviceHandler } which manages the
|
||||
* different power levels specified for a proxy device. It is responsible for parsing the configuration string
|
||||
* provided (from the proxy device configuration). Then it will handle retrieval of any interpolation and
|
||||
* retrieval of the power level given the state (of the proxy device).
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SenseEnergyPowerLevels {
|
||||
public NavigableMap<String, @Nullable QuantityType<Power>> powerStateLevels = Collections.emptyNavigableMap();
|
||||
public List<@Nullable QuantityType<Power>> powerValueLevels = Collections.emptyList();
|
||||
|
||||
@SuppressWarnings("unchecked") // prevent warning in cast to QuantityType<Power> which is guaranteed by specifying
|
||||
// the Units.WATT
|
||||
public void parse(String levels) {
|
||||
String[] splitList = levels.split(",", 0);
|
||||
final Pattern p = Pattern.compile("\\p{Space}*(\\p{Alpha}\\p{Alnum}*)\\p{Space}*=(.+)");
|
||||
|
||||
// extract non-state specified power levels: 0W,1.5,2W
|
||||
powerValueLevels = Arrays.stream(splitList).filter(s -> s.indexOf('=') == -1) //
|
||||
.map(s -> (QuantityType<Power>) parseQuantityType(s, Units.WATT)) //
|
||||
.sorted() //
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// extract state specified power levels
|
||||
powerStateLevels = Arrays.stream(splitList) //
|
||||
.filter(s -> s.indexOf('=') != -1) // "OFF=0, LOW=2W, HIGH=5W", 0, 3); //
|
||||
.map(s -> { //
|
||||
Matcher m = p.matcher(s); //
|
||||
m.matches(); //
|
||||
return m; //
|
||||
}) //
|
||||
.collect(Collectors.toMap( //
|
||||
m -> m.group(1).toUpperCase(), //
|
||||
m -> (QuantityType<Power>) parseQuantityType(m.group(2), Units.WATT), //
|
||||
(existing, replacement) -> existing, // merge function to avoid duplicate keys
|
||||
TreeMap::new));
|
||||
}
|
||||
|
||||
public int getNumValueLevels() {
|
||||
return powerValueLevels.size();
|
||||
}
|
||||
|
||||
public int getNumStateLevels() {
|
||||
return powerStateLevels.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String valueLevels = powerValueLevels.stream() //
|
||||
.filter(Objects::nonNull) //
|
||||
.map(qt -> qt.toString()) //
|
||||
.collect(Collectors.joining(","));
|
||||
|
||||
String stateLevels = powerStateLevels.descendingMap().entrySet().stream() //
|
||||
.filter(kv -> kv.getValue() != null) //
|
||||
.map(kv -> kv.getKey() + "=" + String.valueOf(kv.getValue())) //
|
||||
.collect(Collectors.joining(","));
|
||||
|
||||
return valueLevels.isEmpty() ? stateLevels
|
||||
: stateLevels.isEmpty() ? valueLevels : valueLevels + "," + stateLevels;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public QuantityType<Power> getLevel(int level) {
|
||||
int numNodes = powerValueLevels.size();
|
||||
if (numNodes == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Point2D.Float p0 = new Point2D.Float(0, 0);
|
||||
Point2D.Float p1 = new Point2D.Float(100, getPowerFloatValue(numNodes - 1)); // if only one node, value is set
|
||||
// to node 0
|
||||
|
||||
if (numNodes >= 2) {
|
||||
p1.setLocation(0f, getPowerFloatValue(0));
|
||||
for (int i = 1; i < numNodes; i++) {
|
||||
p0.setLocation(p1);
|
||||
p1.setLocation(i * (100f / (numNodes - 1)), getPowerFloatValue(i));
|
||||
if (level >= p0.getX() && level <= p1.getX()) { // found bounding points
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// p0 and p1 are set at this point
|
||||
float floatLevel = (float) (p0.getY()
|
||||
+ (level - p0.getX()) * (p1.getY() - p0.getY()) / (p1.getX() - p0.getX()));
|
||||
|
||||
QuantityType<Power> qt = new QuantityType<Power>(floatLevel, Units.WATT);
|
||||
|
||||
return qt;
|
||||
}
|
||||
|
||||
/*
|
||||
* get power level for a specific state
|
||||
*/
|
||||
@Nullable
|
||||
public QuantityType<Power> getLevel(String state) {
|
||||
String ucState = state.toUpperCase();
|
||||
QuantityType<Power> result = powerStateLevels.get(ucState) instanceof QuantityType<Power> qt
|
||||
? qt.toUnit(Units.WATT)
|
||||
: null;
|
||||
|
||||
if (result == null) {
|
||||
int intLevel = switch (ucState) {
|
||||
case "ON" -> 100;
|
||||
case "OFF" -> 0;
|
||||
default -> -1;
|
||||
};
|
||||
result = intLevel != -1 ? getLevel(intLevel) : null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private QuantityType<?> parseQuantityType(String s, Unit<?> defaultUnit) {
|
||||
QuantityType<?> qt = new QuantityType<>(s.trim());
|
||||
|
||||
if (qt.getUnit() == Units.ONE) {
|
||||
// assume W unit
|
||||
return new QuantityType<>(qt.floatValue(), defaultUnit);
|
||||
}
|
||||
if (!qt.getUnit().isCompatible(Units.WATT)) {
|
||||
throw new IllegalArgumentException("Incompatible unit: " + qt.getUnit());
|
||||
}
|
||||
return qt;
|
||||
}
|
||||
|
||||
private float getPowerFloatValue(int level) {
|
||||
if (powerValueLevels.get(level) instanceof QuantityType<Power> qt
|
||||
&& qt.toUnit(Units.WATT) instanceof QuantityType<Power> qtW) {
|
||||
return qtW.floatValue();
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.utils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* {@link TpLinkEncryption } provides encryption for TpLink messages
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TpLinkEncryption {
|
||||
private static final int STARTKEY = 0xAB;
|
||||
|
||||
/**
|
||||
* {@link encrypt} will encrypt string to an encrypted byte[]
|
||||
*
|
||||
* @param unencrypted string
|
||||
* @return encrypted byte[]
|
||||
*/
|
||||
public static byte[] encrypt(String unencrypted) {
|
||||
try {
|
||||
return encrypt(unencrypted.getBytes("UTF-8"), unencrypted.length());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link encrypt} will encrypt a byte[] with length l
|
||||
*
|
||||
* @param unencrypted byte[]
|
||||
* @param l length
|
||||
* @return encrypted byte[]
|
||||
*/
|
||||
|
||||
public static byte[] encrypt(byte[] unencrypted, int l) {
|
||||
int length = (l == 0) ? unencrypted.length : l;
|
||||
int key = STARTKEY;
|
||||
|
||||
byte[] encrypted = new byte[length];
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
key = key ^ unencrypted[i];
|
||||
encrypted[i] = (byte) key;
|
||||
}
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link decrypt} will decypt an encrypted byte[] to an unencrypted byte[]
|
||||
*
|
||||
* @param crypted byte[]
|
||||
* @param l length
|
||||
* @return unencrypted byte[]
|
||||
*/
|
||||
|
||||
public static byte[] decrypt(byte[] crypted, int l) {
|
||||
int key = STARTKEY;
|
||||
int a;
|
||||
|
||||
int length = (l == 0) ? crypted.length : l;
|
||||
|
||||
byte[] decrypted = new byte[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
a = key ^ crypted[i];
|
||||
key = crypted[i];
|
||||
decrypted[i] = (byte) a;
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="senseenergy" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>SenseEnergy Binding</name>
|
||||
<description>This is the binding for Sense energy monitor (sense.com).</description>
|
||||
<connection>hybrid</connection>
|
||||
</addon:addon>
|
|
@ -0,0 +1,100 @@
|
|||
# add-on
|
||||
|
||||
addon.senseenergy.name = SenseEnergy Binding
|
||||
addon.senseenergy.description = This is the binding for Sense energy monitor (sense.com).
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.senseenergy.cloud-connector.label = Sense Energy Cloud Connector
|
||||
thing-type.senseenergy.cloud-connector.description = The Sense Home cloud connector establishes connection to the Sense cloud API and services.
|
||||
thing-type.senseenergy.monitor.label = Sense Energy Monitor
|
||||
thing-type.senseenergy.monitor.description = Sense energy monitor instance.
|
||||
thing-type.senseenergy.proxy-device.label = Proxy Device
|
||||
thing-type.senseenergy.proxy-device.description = A proxy device to notify the sense monitor of real-time power draw. All channels are "one-way" in that they only report their settings to Sense.
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.senseenergy.cloud-connector.email.label = Email
|
||||
thing-type.config.senseenergy.cloud-connector.email.description = Sense account email address
|
||||
thing-type.config.senseenergy.cloud-connector.password.label = Password
|
||||
thing-type.config.senseenergy.cloud-connector.password.description = Sense account password
|
||||
thing-type.config.senseenergy.monitor.id.label = ID
|
||||
thing-type.config.senseenergy.monitor.id.description = Device ID (only known from the openHAB log or when devices is discovered).
|
||||
thing-type.config.senseenergy.proxy-device.macAddress.label = Spoof'd MAC Address
|
||||
thing-type.config.senseenergy.proxy-device.macAddress.description = A spoof'ed MAC address for this proxy device.
|
||||
thing-type.config.senseenergy.proxy-device.powerLevels.label = Power Levels
|
||||
thing-type.config.senseenergy.proxy-device.powerLevels.description = Power levels for different states. Examples: "5W" (static full ON power), ".2W, 5W" (for static power range), "OFF=.2W, LOW=2W, HIGH=5W" (for static power in different states), ".2,2W,2.5W,3W" (for non-linear dimmer rage".
|
||||
thing-type.config.senseenergy.proxy-device.senseName.label = Sense Name
|
||||
thing-type.config.senseenergy.proxy-device.senseName.description = Name of device to be used by Sense.
|
||||
thing-type.config.senseenergy.proxy-device.voltage.label = Voltage
|
||||
thing-type.config.senseenergy.proxy-device.voltage.description = Supply voltage for device.
|
||||
|
||||
# channel group types
|
||||
|
||||
channel-group-type.senseenergy.device-template.label = Device
|
||||
channel-group-type.senseenergy.device-template.description = Discovered devices
|
||||
channel-group-type.senseenergy.discovered-devices.label = Discovered Devices
|
||||
channel-group-type.senseenergy.discovered-devices.description = Sense discovered devices
|
||||
channel-group-type.senseenergy.general.label = General Information
|
||||
channel-group-type.senseenergy.general.description = General information about the monitor
|
||||
channel-group-type.senseenergy.proxy-devices.label = Proxy Devices
|
||||
channel-group-type.senseenergy.proxy-devices.description = Proxy devices configured in openHAB which report their power
|
||||
channel-group-type.senseenergy.self-reporting-devices.label = Self-Reporting Devices
|
||||
channel-group-type.senseenergy.self-reporting-devices.description = Devices that self report their power to Sense
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.senseenergy.device-power.label = Power
|
||||
channel-type.senseenergy.device-power.description = Power draw of discovered device.
|
||||
channel-type.senseenergy.device-trigger.label = On Off Trigger
|
||||
channel-type.senseenergy.device-trigger.description = Triggered when the discovered device turns ON and OFF.
|
||||
channel-type.senseenergy.devices-updated-trigger.label = Discovered Devices Updated
|
||||
channel-type.senseenergy.devices-updated-trigger.description = Triggered when the discovered device list has been updated
|
||||
channel-type.senseenergy.frequency.label = Frequency
|
||||
channel-type.senseenergy.frequency.description = Electrical frequency detected by Sense.
|
||||
channel-type.senseenergy.grid-power.label = Grid Power
|
||||
channel-type.senseenergy.grid-power.description = Power consumed from the grid (negative if supplying power to grid)
|
||||
channel-type.senseenergy.leg-1-power.label = Leg 1 Power
|
||||
channel-type.senseenergy.leg-1-power.description = Power detected by the first Sense clamp.
|
||||
channel-type.senseenergy.leg-2-power.label = Leg 2 Power
|
||||
channel-type.senseenergy.leg-2-power.description = Power detected by the second Sense clamp.
|
||||
channel-type.senseenergy.main-power.label = Main Power
|
||||
channel-type.senseenergy.main-power.description = Power detected by the main Sense clamp.
|
||||
channel-type.senseenergy.potential-1.label = Potential 1
|
||||
channel-type.senseenergy.potential-1.description = Potential measured on first 120V branch.
|
||||
channel-type.senseenergy.potential-2.label = Potential 2
|
||||
channel-type.senseenergy.potential-2.description = Potential measured on second 120V branch.
|
||||
channel-type.senseenergy.proxy-device-dimmer.label = Device Dimmer
|
||||
channel-type.senseenergy.proxy-device-dimmer.description = Dimmer to notify the current power. This will report to sense an interpolated value based on the specified levels.
|
||||
channel-type.senseenergy.proxy-device-power.label = Power Level
|
||||
channel-type.senseenergy.proxy-device-power.description = Realtime power to send to Sense. Note, if you are using the Switch, Dimmer or State channels, it is not necessary to use this channel.
|
||||
channel-type.senseenergy.proxy-device-state.label = Device State
|
||||
channel-type.senseenergy.proxy-device-state.description = Current device state. This will report to sense the specified power based on the device state.
|
||||
channel-type.senseenergy.proxy-device-switch.label = Device Switch
|
||||
channel-type.senseenergy.proxy-device-switch.description = OnOff switch to notify when device is On/Off. This will report to sense the full ON or full OFF values specified.
|
||||
channel-type.senseenergy.solar-power.label = Solar Power
|
||||
channel-type.senseenergy.solar-power.description = Power detected by the solar Sense clamp.
|
||||
|
||||
# thing status messages
|
||||
|
||||
offline.configuration-error.bridge-missing = Sense Cloud Connector bridge must be online
|
||||
offline.configuration-error.user-credentials-missing = Missing email and/or password
|
||||
|
||||
# api error conditions
|
||||
|
||||
api.invalid-user-credentials = Invalid user credentials, please check configuration
|
||||
api.response-fail = API response fail
|
||||
api.response-invalid = API response invalid
|
||||
api.rate-limit-exceeded = API rate limit exceeded
|
||||
|
||||
# actions
|
||||
|
||||
actions.description.query-energy-trend = Queries energy trend over a period of time.
|
||||
actions.input.description.scale = Scale to be returned (DAY, WEEK, MONTH, YEAR)
|
||||
actions.input.description.datetime = Restrict the query range to data samples since this datetime.
|
||||
actions.output.description.consumption = the total energy (KWh) used over the scale period.
|
||||
actions.output.description.production = the total energy (KWh) produced over the scale period.
|
||||
actions.output.description.from-grid = the total energy (KWh) from the grid over the scale period.
|
||||
actions.output.description.to-grid = the total energy (KWh) to the grid over the scale period.
|
||||
actions.output.description.net-production = the difference in energy (KWh) between what was produced and consumed during the scale period.
|
||||
actions.output.description.solar-powered = the percent of solar energy production that was directly consumed (not sent to grid) during the scale period.
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="senseenergy"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="cloud-connector">
|
||||
<label>Sense Energy Cloud Connector</label>
|
||||
<description>The Sense Home cloud connector establishes connection to the Sense cloud API and services.</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="email" type="text" required="true">
|
||||
<label>Email</label>
|
||||
<context>text</context>
|
||||
<description>Sense account email address</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<label>Password</label>
|
||||
<context>password</context>
|
||||
<description>Sense account password</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,163 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="senseenergy"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="monitor">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="cloud-connector"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Sense Energy Monitor</label>
|
||||
<description>Sense energy monitor instance.</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="general" typeId="general"/>
|
||||
<channel-group id="discovered-devices" typeId="discovered-devices"/>
|
||||
<channel-group id="self-reporting-devices" typeId="self-reporting-devices"/>
|
||||
<channel-group id="proxy-devices" typeId="proxy-devices"/>
|
||||
</channel-groups>
|
||||
|
||||
<properties>
|
||||
<property name="solarConfigured"></property>
|
||||
<property name="ipAddress"></property>
|
||||
<property name="version"></property>
|
||||
<property name="serial"></property>
|
||||
<property name="ssid"></property>
|
||||
<property name="mac"></property>
|
||||
</properties>
|
||||
|
||||
<representation-property>id</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="id" type="integer">
|
||||
<label>ID</label>
|
||||
<description>Device ID (only known from the openHAB log or when devices is discovered).</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</bridge-type>
|
||||
|
||||
<channel-group-type id="general">
|
||||
<label>General Information</label>
|
||||
<description>General information about the monitor</description>
|
||||
<channels>
|
||||
<channel id="frequency" typeId="frequency"/>
|
||||
<channel id="grid-power" typeId="grid-power"/>
|
||||
<channel id="potential-1" typeId="potential-1"/>
|
||||
<channel id="potential-2" typeId="potential-2"/>
|
||||
<channel id="leg-1-power" typeId="leg-1-power"/>
|
||||
<channel id="leg-2-power" typeId="leg-2-power"/>
|
||||
<channel id="main-power" typeId="main-power"/>
|
||||
<channel id="solar-power" typeId="solar-power"/>
|
||||
<channel id="devices-updated-trigger" typeId="devices-updated-trigger"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="discovered-devices">
|
||||
<label>Discovered Devices</label>
|
||||
<description>Sense discovered devices</description>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="self-reporting-devices">
|
||||
<label>Self-Reporting Devices</label>
|
||||
<description>Devices that self report their power to Sense</description>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="proxy-devices">
|
||||
<label>Proxy Devices</label>
|
||||
<description>Proxy devices configured in openHAB which report their power</description>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="device-template">
|
||||
<label>Device</label>
|
||||
<description>Discovered devices</description>
|
||||
<channels>
|
||||
<channel id="device-power" typeId="device-power"/>
|
||||
<channel id="trigger" typeId="device-trigger"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="potential-1">
|
||||
<item-type unitHint="V">Number:ElectricPotential</item-type>
|
||||
<label>Potential 1</label>
|
||||
<description>Potential measured on first 120V branch.</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="potential-2">
|
||||
<item-type unitHint="V">Number:ElectricPotential</item-type>
|
||||
<label>Potential 2</label>
|
||||
<description>Potential measured on second 120V branch.</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="leg-1-power">
|
||||
<item-type unitHint="W">Number:Power</item-type>
|
||||
<label>Leg 1 Power</label>
|
||||
<description>Power detected by the first Sense clamp.</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="leg-2-power">
|
||||
<item-type unitHint="W">Number:Power</item-type>
|
||||
<label>Leg 2 Power</label>
|
||||
<description>Power detected by the second Sense clamp.</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="main-power">
|
||||
<item-type unitHint="W">Number:Power</item-type>
|
||||
<label>Main Power</label>
|
||||
<description>Power detected by the main Sense clamp.</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="solar-power">
|
||||
<item-type unitHint="W">Number:Power</item-type>
|
||||
<label>Solar Power</label>
|
||||
<description>Power detected by the solar Sense clamp.</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="grid-power">
|
||||
<item-type unitHint="W">Number:Power</item-type>
|
||||
<label>Grid Power</label>
|
||||
<description>Power consumed from the grid (negative if supplying power to grid)</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="devices-updated-trigger" advanced="true">
|
||||
<kind>trigger</kind>
|
||||
<label>Discovered Devices Updated</label>
|
||||
<description>Triggered when the discovered device list has been updated</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="device-power">
|
||||
<item-type unitHint="W">Number:Power</item-type>
|
||||
<label>Power</label>
|
||||
<description>Power draw of discovered device.</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="device-trigger" advanced="true">
|
||||
<kind>trigger</kind>
|
||||
<label>On Off Trigger</label>
|
||||
<description>Triggered when the discovered device turns ON and OFF.</description>
|
||||
<event>
|
||||
<options>
|
||||
<option value="ON">ON</option>
|
||||
<option value="OFF">OFF</option>
|
||||
</options>
|
||||
</event>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="frequency" advanced="true">
|
||||
<item-type unitHint="Hz">Number:Frequency</item-type>
|
||||
<label>Frequency</label>
|
||||
<description>Electrical frequency detected by Sense.</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,85 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="senseenergy"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- Sense Home Energy Monitor Report Thing -->
|
||||
<thing-type id="proxy-device">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="monitor"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Proxy Device</label>
|
||||
<description>
|
||||
A proxy device to notify the sense monitor of real-time power draw. All channels are "one-way" in that
|
||||
they only report their settings to Sense.
|
||||
</description>
|
||||
|
||||
<channels>
|
||||
<channel id="proxy-device-power" typeId="proxy-device-power"/>
|
||||
<channel id="proxy-device-switch" typeId="proxy-device-switch"/>
|
||||
<channel id="proxy-device-dimmer" typeId="proxy-device-dimmer"/>
|
||||
<channel id="proxy-device-state" typeId="proxy-device-state"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="powerLevels" type="text">
|
||||
<label>Power Levels</label>
|
||||
<description>Power levels for different states. Examples: "5W" (static full ON power), ".2W, 5W" (for static power
|
||||
range), "OFF=.2W, LOW=2W, HIGH=5W" (for static power in different states), ".2,2W,2.5W,3W" (for non-linear dimmer
|
||||
rage".</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="voltage" type="decimal" unit="V">
|
||||
<label>Voltage</label>
|
||||
<description>Supply voltage for device.</description>
|
||||
<unitLabel>V</unitLabel>
|
||||
<default>120</default>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
|
||||
<parameter name="macAddress" type="text">
|
||||
<context>network-address</context>
|
||||
<label>Spoof'd MAC Address</label>
|
||||
<description>A spoof'ed MAC address for this proxy device.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="senseName" type="text">
|
||||
<label>Sense Name</label>
|
||||
<description>Name of device to be used by Sense.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="proxy-device-power">
|
||||
<item-type>Number:Power</item-type>
|
||||
<label>Power Level</label>
|
||||
<description>Realtime power to send to Sense. Note, if you are using the Switch, Dimmer or State channels, it is not
|
||||
necessary to use this channel.</description>
|
||||
<state pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="proxy-device-switch">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Device Switch</label>
|
||||
<description>OnOff switch to notify when device is On/Off. This will report to sense the full ON or full OFF values
|
||||
specified.
|
||||
</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="proxy-device-dimmer">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Device Dimmer</label>
|
||||
<description>Dimmer to notify the current power. This will report to sense an interpolated value based on the
|
||||
specified levels.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="proxy-device-state">
|
||||
<item-type>String</item-type>
|
||||
<label>Device State</label>
|
||||
<description>Current device state. This will report to sense the specified power based on the device state.</description>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,98 @@
|
|||
// SenseEnerbyApiAuthenticate
|
||||
|
||||
{
|
||||
"authorized": true,
|
||||
"account_id": xxxx,
|
||||
"user_id": xxxx,
|
||||
"access_token": "t1.v2.xxx",
|
||||
"settings": {
|
||||
"user_id": xxxx,
|
||||
"settings": {
|
||||
"notifications": {
|
||||
"xxxx": {
|
||||
"new_named_device_push": true,
|
||||
"new_named_device_email": true,
|
||||
"monitor_offline_push": true,
|
||||
"monitor_offline_email": true,
|
||||
"monitor_monthly_email": true,
|
||||
"always_on_change_push": true,
|
||||
"comparison_change_push": true,
|
||||
"new_peak_push": true,
|
||||
"new_peak_email": false,
|
||||
"monthly_change_push": true,
|
||||
"weekly_change_push": false,
|
||||
"daily_change_push": false,
|
||||
"generator_on_push": true,
|
||||
"generator_off_push": true,
|
||||
"time_of_use": true,
|
||||
"grid_outage_push": true,
|
||||
"grid_restored_push": true,
|
||||
"relay_update_available_push": true,
|
||||
"relay_update_installed_push": true,
|
||||
"new_features_and_offers_push": true
|
||||
}
|
||||
},
|
||||
"labs_enabled": true,
|
||||
"hide_trends_carbon_card": false,
|
||||
"ohm_connect_status": "eligible"
|
||||
},
|
||||
"version": 1
|
||||
},
|
||||
"refresh_token": "xxxx",
|
||||
"monitors": [
|
||||
{
|
||||
"id": xxxx,
|
||||
"date_created": "2024-04-01T17:12:45.000Z",
|
||||
"serial_number": "N327004101",
|
||||
"time_zone": "America/Los_Angeles",
|
||||
"solar_connected": true,
|
||||
"solar_configured": true,
|
||||
"online": false,
|
||||
"attributes": {
|
||||
"id": xxxx,
|
||||
"name": "",
|
||||
"state": "CA",
|
||||
"cost": 20.77,
|
||||
"sell_back_rate": 20.77,
|
||||
"user_set_cost": false,
|
||||
"cycle_start": null,
|
||||
"basement_type": "No basement",
|
||||
"home_size_type": "3250 sq. ft",
|
||||
"home_type": "Single family",
|
||||
"number_of_occupants": "3",
|
||||
"occupancy_type": "Full-time",
|
||||
"year_built_type": "1980s",
|
||||
"basement_type_key": null,
|
||||
"home_size_type_key": null,
|
||||
"home_type_key": null,
|
||||
"occupancy_type_key": null,
|
||||
"year_built_type_key": null,
|
||||
"address": null,
|
||||
"city": null,
|
||||
"postal_code": "xxxx",
|
||||
"electricity_cost": null,
|
||||
"show_cost": true,
|
||||
"tou_enabled": false,
|
||||
"solar_tou_enabled": false,
|
||||
"power_region": null,
|
||||
"to_grid_threshold": null,
|
||||
"panel": null,
|
||||
"home_info_survey_progress": "COMPLETED",
|
||||
"device_survey_progress": "COMPLETED",
|
||||
"user_set_sell_back_rate": true
|
||||
},
|
||||
"signal_check_completed_time": "2024-04-01T17:31:55.000Z",
|
||||
"data_sharing": [],
|
||||
"ethernet_supported": false,
|
||||
"power_over_ethernet_supported": false,
|
||||
"aux_ignore": false,
|
||||
"aux_port": "solar",
|
||||
"hardware_type": "monitor",
|
||||
"zigbee_supported": false
|
||||
}
|
||||
],
|
||||
"bridge_server": "wss://mb1.home.sense.com",
|
||||
"date_created": "2024-04-01T17:15:13.000Z",
|
||||
"totp_enabled": false,
|
||||
"ab_cohort": "energyhog_2"
|
||||
}
|
|
@ -0,0 +1,716 @@
|
|||
// SenseEnergyApiDevice
|
||||
|
||||
[
|
||||
{
|
||||
"id": "0e64ad2b",
|
||||
"name": "AC 2",
|
||||
"icon": "ac",
|
||||
"tags": {
|
||||
"Alertable": "true",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-05-09T04:16:08.000Z",
|
||||
"DateFirstUsage": "2024-04-02",
|
||||
"DefaultUserDeviceType": "AC",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "true",
|
||||
"ModelCreatedVersion": "21",
|
||||
"ModelUpdatedVersion": "27",
|
||||
"name_useredit": "false",
|
||||
"OriginalName": "AC 2",
|
||||
"PeerNames": [
|
||||
{
|
||||
"Name": "Gas Dryer",
|
||||
"UserDeviceType": "GasDryer",
|
||||
"Percent": 97.0,
|
||||
"Icon": "washer",
|
||||
"UserDeviceTypeDisplayString": "Gas Dryer"
|
||||
},
|
||||
{
|
||||
"Name": "Furnace",
|
||||
"UserDeviceType": "Furnace",
|
||||
"Percent": 1.0,
|
||||
"Icon": "heat",
|
||||
"UserDeviceTypeDisplayString": "Furnace"
|
||||
},
|
||||
{
|
||||
"Name": "Pump",
|
||||
"UserDeviceType": "Pump",
|
||||
"Percent": 1.0,
|
||||
"Icon": "pump",
|
||||
"UserDeviceTypeDisplayString": "Pump"
|
||||
},
|
||||
{
|
||||
"Name": "Washer",
|
||||
"UserDeviceType": "Washer",
|
||||
"Percent": 1.0,
|
||||
"Icon": "washer",
|
||||
"UserDeviceTypeDisplayString": "Washer"
|
||||
}
|
||||
],
|
||||
"Pending": "false",
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "true",
|
||||
"TimelineDefault": "true",
|
||||
"Type": "AC",
|
||||
"UserDeletable": "true",
|
||||
"UserDeviceType": "AC",
|
||||
"UserDeviceTypeDisplayString": "AC",
|
||||
"UserEditable": "true",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowInDeviceList": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "zDcE5VOr",
|
||||
"name": "Sense Whole House Proxy Fan",
|
||||
"icon": "plug",
|
||||
"tags": {
|
||||
"Alertable": "false",
|
||||
"ControlCapabilities": [
|
||||
"OnOff",
|
||||
"StandbyThreshold"
|
||||
],
|
||||
"DateCreated": "2024-09-22T03:13:45.000Z",
|
||||
"DefaultUserDeviceType": "SmartPlug",
|
||||
"DeviceListAllowed": "false",
|
||||
"DUID": "53:75:31:70:55:53",
|
||||
"IntegratedDeviceType": "IntegratedSmartPlug",
|
||||
"IntegrationType": "TPLink",
|
||||
"name_useredit": "false",
|
||||
"OriginalName": "Sense Whole House Proxy Fan",
|
||||
"Revoked": "false",
|
||||
"SmartPlugModel": "TP-Link Kasa HS110",
|
||||
"SSIEnabled": "true",
|
||||
"SSIModel": "SelfReporting",
|
||||
"TimelineAllowed": "false",
|
||||
"TimelineDefault": "false",
|
||||
"UserDeletable": "false",
|
||||
"UserDeleted": "true",
|
||||
"UserDeviceType": "SmartPlug",
|
||||
"UserDeviceTypeDisplayString": "Smart Plug",
|
||||
"UserEditable": "false",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "false",
|
||||
"UserShowInDeviceList": "true",
|
||||
"UserVisibleDeviceId": "53:75:31:70:55:53"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2681a800",
|
||||
"name": "Toaster oven",
|
||||
"icon": "toaster_oven",
|
||||
"tags": {
|
||||
"Alertable": "true",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-04-02T20:03:12.000Z",
|
||||
"DateFirstUsage": "2024-04-01",
|
||||
"DefaultUserDeviceType": "MysteryHeat",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "true",
|
||||
"ModelCreatedVersion": "2",
|
||||
"ModelUpdatedVersion": "27",
|
||||
"name_useredit": "true",
|
||||
"OriginalName": "Heat 1",
|
||||
"PeerNames": [
|
||||
{
|
||||
"Name": "Toaster Oven",
|
||||
"UserDeviceType": "ToasterOven",
|
||||
"Percent": 82.0,
|
||||
"Icon": "toaster_oven",
|
||||
"UserDeviceTypeDisplayString": "Toaster Oven"
|
||||
},
|
||||
{
|
||||
"Name": "Tea Kettle",
|
||||
"UserDeviceType": "TeaKettle",
|
||||
"Percent": 6.0,
|
||||
"Icon": "kettle",
|
||||
"UserDeviceTypeDisplayString": "Tea Kettle"
|
||||
},
|
||||
{
|
||||
"Name": "Printer",
|
||||
"UserDeviceType": "Printer",
|
||||
"Percent": 2.0,
|
||||
"Icon": "printer",
|
||||
"UserDeviceTypeDisplayString": "Printer"
|
||||
},
|
||||
{
|
||||
"Name": "Hair Dryer",
|
||||
"UserDeviceType": "HairDryer",
|
||||
"Percent": 2.0,
|
||||
"Icon": "hair_dryer",
|
||||
"UserDeviceTypeDisplayString": "Hair Dryer"
|
||||
}
|
||||
],
|
||||
"Pending": "false",
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "true",
|
||||
"TimelineDefault": "true",
|
||||
"Type": "UnknownHeat",
|
||||
"UserDeletable": "true",
|
||||
"UserDeviceType": "ToasterOven",
|
||||
"UserDeviceTypeDisplayString": "Toaster Oven",
|
||||
"UserEditable": "true",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowInDeviceList": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "37cec5c4",
|
||||
"name": "Furnace",
|
||||
"icon": "heat",
|
||||
"tags": {
|
||||
"Alertable": "true",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-04-19T02:12:31.000Z",
|
||||
"DateFirstUsage": "2024-04-02",
|
||||
"DefaultUserDeviceType": "Furnace",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "true",
|
||||
"ModelCreatedVersion": "16",
|
||||
"ModelUpdatedVersion": "27",
|
||||
"name_useredit": "false",
|
||||
"OriginalName": "Furnace",
|
||||
"PeerNames": [
|
||||
{
|
||||
"Name": "Furnace",
|
||||
"UserDeviceType": "Furnace",
|
||||
"Percent": 86.0,
|
||||
"Icon": "heat",
|
||||
"UserDeviceTypeDisplayString": "Furnace"
|
||||
},
|
||||
{
|
||||
"Name": "Fan",
|
||||
"UserDeviceType": "Fan",
|
||||
"Percent": 7.0,
|
||||
"Icon": "fan",
|
||||
"UserDeviceTypeDisplayString": "Fan"
|
||||
},
|
||||
{
|
||||
"Name": "Central Heat",
|
||||
"UserDeviceType": "CentralHeat",
|
||||
"Percent": 5.0,
|
||||
"Icon": "heat",
|
||||
"UserDeviceTypeDisplayString": "Central Heat"
|
||||
},
|
||||
{
|
||||
"Name": "AC",
|
||||
"UserDeviceType": "AC",
|
||||
"Percent": 1.0,
|
||||
"Icon": "ac",
|
||||
"UserDeviceTypeDisplayString": "AC"
|
||||
}
|
||||
],
|
||||
"Pending": "false",
|
||||
"PreselectionIndex": 0,
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "true",
|
||||
"TimelineDefault": "false",
|
||||
"Type": "Furnace",
|
||||
"UserDeletable": "true",
|
||||
"UserDeviceType": "Furnace",
|
||||
"UserDeviceTypeDisplayString": "Furnace",
|
||||
"UserEditable": "true",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowInDeviceList": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "57aca87d",
|
||||
"name": "Microwave",
|
||||
"icon": "microwave",
|
||||
"tags": {
|
||||
"Alertable": "true",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-04-22T18:01:40.000Z",
|
||||
"DateFirstUsage": "2024-04-01",
|
||||
"DefaultUserDeviceType": "Microwave",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "true",
|
||||
"ModelCreatedVersion": "18",
|
||||
"ModelUpdatedVersion": "27",
|
||||
"name_useredit": "false",
|
||||
"OriginalName": "Microwave",
|
||||
"PeerNames": [
|
||||
{
|
||||
"Name": "Microwave",
|
||||
"UserDeviceType": "Microwave",
|
||||
"Percent": 100.0,
|
||||
"Icon": "microwave",
|
||||
"UserDeviceTypeDisplayString": "Microwave"
|
||||
}
|
||||
],
|
||||
"Pending": "false",
|
||||
"PreselectionIndex": 0,
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "true",
|
||||
"TimelineDefault": "true",
|
||||
"Type": "Microwave",
|
||||
"UserDeletable": "true",
|
||||
"UserDeviceType": "Microwave",
|
||||
"UserDeviceTypeDisplayString": "Microwave",
|
||||
"UserEditable": "true",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowInDeviceList": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "a6b4aa4d",
|
||||
"name": "Backyard Spotlight",
|
||||
"icon": "lightbulb",
|
||||
"tags": {
|
||||
"Alertable": "true",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-06-07T12:10:01.000Z",
|
||||
"DateFirstUsage": "2024-04-06",
|
||||
"DefaultUserDeviceType": "Light",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "true",
|
||||
"ModelCreatedVersion": "24",
|
||||
"ModelUpdatedVersion": "27",
|
||||
"name_useredit": "true",
|
||||
"OriginalName": "Light 1",
|
||||
"PeerNames": [
|
||||
{
|
||||
"Name": "Light",
|
||||
"UserDeviceType": "Light",
|
||||
"Percent": 87.0,
|
||||
"Icon": "lightbulb",
|
||||
"UserDeviceTypeDisplayString": "Light"
|
||||
},
|
||||
{
|
||||
"Name": "TV/Monitor",
|
||||
"UserDeviceType": "TV",
|
||||
"Percent": 7.0,
|
||||
"Icon": "tv",
|
||||
"UserDeviceTypeDisplayString": "TV/Monitor"
|
||||
},
|
||||
{
|
||||
"Name": "Appliance Light",
|
||||
"UserDeviceType": "ApplianceLight",
|
||||
"Percent": 3.0,
|
||||
"Icon": "lightbulb",
|
||||
"UserDeviceTypeDisplayString": "Appliance Light"
|
||||
},
|
||||
{
|
||||
"Name": "Water Dispenser",
|
||||
"UserDeviceType": "WaterDispenser",
|
||||
"Percent": 1.0,
|
||||
"Icon": "socket",
|
||||
"UserDeviceTypeDisplayString": "Water Dispenser"
|
||||
}
|
||||
],
|
||||
"Pending": "false",
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "true",
|
||||
"TimelineDefault": "true",
|
||||
"Type": "Lighting",
|
||||
"UserDeletable": "true",
|
||||
"UserDeviceType": "Light",
|
||||
"UserDeviceTypeDisplayString": "Light",
|
||||
"UserEditable": "true",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowInDeviceList": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "a76c2ab2",
|
||||
"name": "Garage door",
|
||||
"icon": "garage",
|
||||
"tags": {
|
||||
"Alertable": "true",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-04-19T02:12:31.000Z",
|
||||
"DateFirstUsage": "2024-04-01",
|
||||
"DefaultUserDeviceType": "GarageDoor",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "true",
|
||||
"ModelCreatedVersion": "16",
|
||||
"ModelUpdatedVersion": "27",
|
||||
"name_useredit": "false",
|
||||
"OriginalName": "Garage door",
|
||||
"PeerNames": [
|
||||
{
|
||||
"Name": "Garage Door",
|
||||
"UserDeviceType": "GarageDoor",
|
||||
"Percent": 99.0,
|
||||
"Icon": "garage",
|
||||
"UserDeviceTypeDisplayString": "Garage Door"
|
||||
},
|
||||
{
|
||||
"Name": "Pump",
|
||||
"UserDeviceType": "Pump",
|
||||
"Percent": 1.0,
|
||||
"Icon": "pump",
|
||||
"UserDeviceTypeDisplayString": "Pump"
|
||||
}
|
||||
],
|
||||
"Pending": "false",
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "true",
|
||||
"TimelineDefault": "true",
|
||||
"Type": "GarageDoor",
|
||||
"UserDeletable": "true",
|
||||
"UserDeviceType": "GarageDoor",
|
||||
"UserDeviceTypeDisplayString": "Garage Door",
|
||||
"UserEditable": "true",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowInDeviceList": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "always_on",
|
||||
"name": "Always On",
|
||||
"icon": "alwayson",
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "AlwaysOn",
|
||||
"DeviceListAllowed": "true",
|
||||
"TimelineAllowed": "false",
|
||||
"UserDeleted": "false",
|
||||
"UserDeviceType": "AlwaysOn",
|
||||
"UserDeviceTypeDisplayString": "Always On",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "db3fc2f4",
|
||||
"name": "Mystery Device 1",
|
||||
"icon": "socket",
|
||||
"tags": {
|
||||
"Alertable": "true",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-04-08T05:41:52.000Z",
|
||||
"DateFirstUsage": "2024-04-01",
|
||||
"DefaultUserDeviceType": "MysteryDevice",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "true",
|
||||
"ModelCreatedVersion": "11",
|
||||
"ModelUpdatedVersion": "27",
|
||||
"name_useredit": "false",
|
||||
"OriginalName": "Mystery Device 1",
|
||||
"PeerNames": [
|
||||
{
|
||||
"Name": "Light",
|
||||
"UserDeviceType": "Light",
|
||||
"Percent": 74.0,
|
||||
"Icon": "lightbulb",
|
||||
"UserDeviceTypeDisplayString": "Light"
|
||||
},
|
||||
{
|
||||
"Name": "Aquarium",
|
||||
"UserDeviceType": "Aquarium",
|
||||
"Percent": 6.0,
|
||||
"Icon": "aquarium",
|
||||
"UserDeviceTypeDisplayString": "Aquarium"
|
||||
},
|
||||
{
|
||||
"Name": "Furnace",
|
||||
"UserDeviceType": "Furnace",
|
||||
"Percent": 2.0,
|
||||
"Icon": "heat",
|
||||
"UserDeviceTypeDisplayString": "Furnace"
|
||||
},
|
||||
{
|
||||
"Name": "Circulator Pump",
|
||||
"UserDeviceType": "CirculatorPump",
|
||||
"Percent": 2.0,
|
||||
"Icon": "pump",
|
||||
"UserDeviceTypeDisplayString": "Circulator Pump"
|
||||
},
|
||||
{
|
||||
"Name": "Computer",
|
||||
"UserDeviceType": "Computer",
|
||||
"Percent": 2.0,
|
||||
"Icon": "computer",
|
||||
"UserDeviceTypeDisplayString": "Computer"
|
||||
},
|
||||
{
|
||||
"Name": "AC",
|
||||
"UserDeviceType": "AC",
|
||||
"Percent": 1.0,
|
||||
"Icon": "ac",
|
||||
"UserDeviceTypeDisplayString": "AC"
|
||||
}
|
||||
],
|
||||
"Pending": "false",
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "true",
|
||||
"TimelineDefault": "true",
|
||||
"Type": "Unknown",
|
||||
"UserDeletable": "true",
|
||||
"UserDeviceType": "MysteryDevice",
|
||||
"UserDeviceTypeDisplayString": "Mystery Device",
|
||||
"UserEditable": "true",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowInDeviceList": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e329c895",
|
||||
"name": "Wine Cooler",
|
||||
"icon": "ac",
|
||||
"tags": {
|
||||
"Alertable": "true",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-04-11T09:28:45.000Z",
|
||||
"DateFirstUsage": "2024-04-01",
|
||||
"DefaultUserDeviceType": "AC",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "true",
|
||||
"ModelCreatedVersion": "12",
|
||||
"ModelUpdatedVersion": "27",
|
||||
"name_useredit": "true",
|
||||
"NameUserGuess": "false",
|
||||
"OriginalName": "AC",
|
||||
"PeerNames": [
|
||||
{
|
||||
"Name": "Dehumidifier",
|
||||
"UserDeviceType": "Dehumidifier",
|
||||
"Percent": 33.0,
|
||||
"Icon": "dehumidifier",
|
||||
"UserDeviceTypeDisplayString": "Dehumidifier"
|
||||
},
|
||||
{
|
||||
"Name": "AC",
|
||||
"UserDeviceType": "AC",
|
||||
"Percent": 22.0,
|
||||
"Icon": "ac",
|
||||
"UserDeviceTypeDisplayString": "AC"
|
||||
},
|
||||
{
|
||||
"Name": "Fridge",
|
||||
"UserDeviceType": "Fridge",
|
||||
"Percent": 14.0,
|
||||
"Icon": "fridge",
|
||||
"UserDeviceTypeDisplayString": "Fridge"
|
||||
},
|
||||
{
|
||||
"Name": "Freezer",
|
||||
"UserDeviceType": "Freezer",
|
||||
"Percent": 4.0,
|
||||
"Icon": "freezer",
|
||||
"UserDeviceTypeDisplayString": "Freezer"
|
||||
},
|
||||
{
|
||||
"Name": "Furnace",
|
||||
"UserDeviceType": "Furnace",
|
||||
"Percent": 4.0,
|
||||
"Icon": "heat",
|
||||
"UserDeviceTypeDisplayString": "Furnace"
|
||||
},
|
||||
{
|
||||
"Name": "AC",
|
||||
"UserDeviceType": "AC",
|
||||
"Percent": 3.0,
|
||||
"Icon": "ac",
|
||||
"UserDeviceTypeDisplayString": "AC"
|
||||
}
|
||||
],
|
||||
"Pending": "false",
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "true",
|
||||
"TimelineDefault": "true",
|
||||
"Type": "WindowAC",
|
||||
"UserDeletable": "true",
|
||||
"UserDeviceType": "AC",
|
||||
"UserDeviceTypeDisplayString": "AC",
|
||||
"UserEditable": "true",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowBubble": "true",
|
||||
"UserShowInDeviceList": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Pgz8OdRP",
|
||||
"name": "Oven",
|
||||
"icon": "stove",
|
||||
"tags": {
|
||||
"Alertable": "true",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-05-09T04:16:08.000Z",
|
||||
"DateFirstUsage": "2024-04-02",
|
||||
"DefaultUserDeviceType": "MysteryHeat",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "true",
|
||||
"MergedDevices": "0988f51e,fc5b120b",
|
||||
"ModelCreatedVersion": "21",
|
||||
"name_useredit": "true",
|
||||
"OriginalName": "Heat 3",
|
||||
"PeerNames": [],
|
||||
"Pending": "false",
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "true",
|
||||
"TimelineDefault": "true",
|
||||
"Type": "UnknownHeat",
|
||||
"UserDeletable": "true",
|
||||
"UserDeviceType": "Oven",
|
||||
"UserDeviceTypeDisplayString": "Oven",
|
||||
"UserEditable": "true",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowInDeviceList": "true",
|
||||
"Virtual": "true"
|
||||
},
|
||||
"make": "Decor"
|
||||
},
|
||||
{
|
||||
"id": "solar",
|
||||
"name": "Solar",
|
||||
"icon": "solar_alt",
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "Solar",
|
||||
"DeviceListAllowed": "false",
|
||||
"TimelineAllowed": "false",
|
||||
"UserDeleted": "false",
|
||||
"UserDeviceType": "Solar",
|
||||
"UserDeviceTypeDisplayString": "Solar",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "unknown",
|
||||
"name": "Other",
|
||||
"icon": "home",
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "Unknown",
|
||||
"DeviceListAllowed": "true",
|
||||
"TimelineAllowed": "false",
|
||||
"UserDeleted": "false",
|
||||
"UserDeviceType": "Unknown",
|
||||
"UserDeviceTypeDisplayString": "Unknown",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pd-9asJf4FK",
|
||||
"name": "Electric Vehicle",
|
||||
"icon": "car",
|
||||
"monitor_id": 869850,
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "ElectricVehicle",
|
||||
"DeviceListAllowed": "true",
|
||||
"Stage": "Tracking",
|
||||
"UserAdded": "false",
|
||||
"UserDeviceType": "ElectricVehicle",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
},
|
||||
"type": "ElectricVehicle",
|
||||
"count": 1,
|
||||
"stage": "Tracking",
|
||||
"last_updated": "2024-08-13T00:00:00.000Z",
|
||||
"realtime_devices": [],
|
||||
"deleted": null
|
||||
},
|
||||
{
|
||||
"id": "pd-iw0KZlLV",
|
||||
"name": "Washer",
|
||||
"icon": "washer",
|
||||
"monitor_id": 869850,
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "Washer",
|
||||
"DeviceListAllowed": "true",
|
||||
"Stage": "Inventory",
|
||||
"UserAdded": "false",
|
||||
"UserDeviceType": "Washer",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
},
|
||||
"type": "Washer",
|
||||
"count": 1,
|
||||
"stage": "Inventory",
|
||||
"realtime_devices": [],
|
||||
"deleted": null
|
||||
},
|
||||
{
|
||||
"id": "pd-nWUFUBc0",
|
||||
"name": "Hot Tub",
|
||||
"icon": "hot_tub",
|
||||
"monitor_id": 869850,
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "HotTub",
|
||||
"DeviceListAllowed": "true",
|
||||
"Stage": "Inventory",
|
||||
"UserAdded": "false",
|
||||
"UserDeviceType": "HotTub",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
},
|
||||
"type": "HotTub",
|
||||
"count": 1,
|
||||
"stage": "Inventory",
|
||||
"realtime_devices": [],
|
||||
"deleted": null
|
||||
},
|
||||
{
|
||||
"id": "pd-QMAly6Xm",
|
||||
"name": "Dishwasher",
|
||||
"icon": "dishes",
|
||||
"monitor_id": 869850,
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "Dishwasher",
|
||||
"DeviceListAllowed": "true",
|
||||
"Stage": "Inventory",
|
||||
"UserAdded": "false",
|
||||
"UserDeviceType": "Dishwasher",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
},
|
||||
"type": "Dishwasher",
|
||||
"count": 1,
|
||||
"stage": "Inventory",
|
||||
"realtime_devices": [],
|
||||
"deleted": null
|
||||
},
|
||||
{
|
||||
"id": "pd-Rj3WQa1s",
|
||||
"name": "Fridges",
|
||||
"icon": "fridge",
|
||||
"monitor_id": 869850,
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "Fridge",
|
||||
"DeviceListAllowed": "true",
|
||||
"Stage": "Tracking",
|
||||
"UserAdded": "false",
|
||||
"UserDeviceType": "Fridge",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
},
|
||||
"type": "Fridge",
|
||||
"count": 2,
|
||||
"stage": "Tracking",
|
||||
"last_updated": "2024-08-13T00:00:00.000Z",
|
||||
"realtime_devices": [],
|
||||
"deleted": null
|
||||
},
|
||||
{
|
||||
"id": "pd-Z4ZMTVq5",
|
||||
"name": "Pool Pump",
|
||||
"icon": "pump",
|
||||
"monitor_id": 869850,
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "PoolPump",
|
||||
"DeviceListAllowed": "true",
|
||||
"Stage": "Inventory",
|
||||
"UserAdded": "false",
|
||||
"UserDeviceType": "PoolPump",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
},
|
||||
"type": "PoolPump",
|
||||
"count": 1,
|
||||
"stage": "Inventory",
|
||||
"realtime_devices": [],
|
||||
"deleted": null
|
||||
}
|
||||
]
|
|
@ -0,0 +1,7 @@
|
|||
// SenseEnergyApiDeviceDetection
|
||||
|
||||
"device_detection": {
|
||||
"in_progress":[],
|
||||
"found":[],
|
||||
"num_detected":15
|
||||
},
|
|
@ -0,0 +1,70 @@
|
|||
// SenseEnergyApiDeviceTags
|
||||
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "ElectricVehicle",
|
||||
"DeviceListAllowed": "true",
|
||||
"Stage": "Tracking",
|
||||
"UserAdded": "false",
|
||||
"UserDeviceType": "ElectricVehicle",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
"Alertable": "false",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-05-09T04:16:08.000Z",
|
||||
"DateFirstUsage": "2024-04-02",
|
||||
"DefaultUserDeviceType": "AC",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "false",
|
||||
"ModelCreatedVersion": "21",
|
||||
"ModelUpdatedVersion": "28",
|
||||
"name_useredit": "true",
|
||||
"OriginalName": "AC 2",
|
||||
"Pending": "false",
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "false",
|
||||
"TimelineDefault": "false",
|
||||
"Type": "AC",
|
||||
"UserDeletable": "true",
|
||||
"UserDeleted": "true",
|
||||
"UserDeviceType": "OtherDevice",
|
||||
"UserDeviceTypeDisplayString": "Other Device",
|
||||
"UserEditable": "false",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowInDeviceList": "true"
|
||||
},
|
||||
{
|
||||
"id": "zDcE5VOr",
|
||||
"name": "Sense Whole House Proxy Fan",
|
||||
"icon": "plug",
|
||||
"tags": {
|
||||
"Alertable": "false",
|
||||
"ControlCapabilities": [
|
||||
"OnOff",
|
||||
"StandbyThreshold"
|
||||
],
|
||||
"DateCreated": "2024-09-22T03:13:45.000Z",
|
||||
"DefaultUserDeviceType": "SmartPlug",
|
||||
"DeviceListAllowed": "false",
|
||||
"DUID": "53:75:31:70:55:53",
|
||||
"IntegratedDeviceType": "IntegratedSmartPlug",
|
||||
"IntegrationType": "TPLink",
|
||||
"name_useredit": "false",
|
||||
"OriginalName": "Sense Whole House Proxy Fan",
|
||||
"Revoked": "false",
|
||||
"SmartPlugModel": "TP-Link Kasa HS110",
|
||||
"SSIEnabled": "true",
|
||||
"SSIModel": "SelfReporting",
|
||||
"TimelineAllowed": "false",
|
||||
"TimelineDefault": "false",
|
||||
"UserDeletable": "false",
|
||||
"UserDeleted": "true",
|
||||
"UserDeviceType": "SmartPlug",
|
||||
"UserDeviceTypeDisplayString": "Smart Plug",
|
||||
"UserEditable": "false",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "false",
|
||||
"UserShowInDeviceList": "true",
|
||||
"UserVisibleDeviceId": "53:75:31:70:55:53"
|
||||
}
|
||||
},
|
|
@ -0,0 +1,829 @@
|
|||
// SenseEnergyApiGetTrends
|
||||
|
||||
{
|
||||
"steps":24,
|
||||
"start":"2024-08-29T07:00:00.000Z",
|
||||
"end":"2024-08-30T07:00:00.000Z",
|
||||
"consumption":{
|
||||
"total":18.861498,
|
||||
"totals":[
|
||||
1.179842,
|
||||
0.83928496,
|
||||
0.860291,
|
||||
1.140934,
|
||||
0.544447,
|
||||
0.562177,
|
||||
0.804201,
|
||||
0.544805,
|
||||
0.558279,
|
||||
0.846418,
|
||||
2.193837,
|
||||
1.217603,
|
||||
1.260953,
|
||||
1.5024201,
|
||||
1.303495,
|
||||
0.503124,
|
||||
0.832463,
|
||||
0.52909696,
|
||||
0.53581405,
|
||||
1.102013,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"devices":[
|
||||
{
|
||||
"id":"LpzF0vGG",
|
||||
"name":"Wine Cooler",
|
||||
"icon":"ac",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"AlwaysOn":"false",
|
||||
"DateCreated":"2024-04-11T09:28:45.000Z",
|
||||
"DateFirstUsage":"2024-04-01",
|
||||
"DefaultUserDeviceType":"AC",
|
||||
"DeployToMonitor":"true",
|
||||
"DeviceListAllowed":"true",
|
||||
"MergedDevices":"db3fc2f4,e329c895",
|
||||
"ModelCreatedVersion":"12",
|
||||
"ModelUpdatedVersion":"28",
|
||||
"name_useredit":"true",
|
||||
"NameUserGuess":"false",
|
||||
"OriginalName":"AC",
|
||||
"PeerNames":[
|
||||
|
||||
],
|
||||
"Pending":"false",
|
||||
"Revoked":"false",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"Type":"WindowAC",
|
||||
"UserDeletable":"true",
|
||||
"UserDeviceType":"AC",
|
||||
"UserDeviceTypeDisplayString":"AC",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"true",
|
||||
"UserShowBubble":"true",
|
||||
"UserShowInDeviceList":"true",
|
||||
"Virtual":"true"
|
||||
},
|
||||
"history":[
|
||||
0.37245202,
|
||||
0.01909,
|
||||
0.020947,
|
||||
0.368776,
|
||||
0.019066999,
|
||||
0.025436,
|
||||
0.28254902,
|
||||
0.019898001,
|
||||
0.019055,
|
||||
0.170864,
|
||||
0.14370401,
|
||||
0.019107,
|
||||
0.059858,
|
||||
0.296629,
|
||||
0.019107,
|
||||
0.019064,
|
||||
0.345176,
|
||||
0.018984,
|
||||
0.040691003,
|
||||
0.364613,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.110211134,
|
||||
"total_kwh":2.6450672,
|
||||
"total_cost":53,
|
||||
"pct":14.0,
|
||||
"cost_history":[
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
3,
|
||||
0,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
7,
|
||||
0,
|
||||
1,
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"wIpu0Jjw",
|
||||
"name":"Entertainment Center",
|
||||
"icon":"plug",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"ControlCapabilities":[
|
||||
"OnOff",
|
||||
"StandbyThreshold"
|
||||
],
|
||||
"DateCreated":"2024-08-22T19:23:30.000Z",
|
||||
"DefaultUserDeviceType":"SmartPlug",
|
||||
"DeviceListAllowed":"true",
|
||||
"DUID":"D8:44:89:30:93:F6",
|
||||
"IntegratedDeviceType":"IntegratedSmartPlug",
|
||||
"IntegrationType":"TPLink",
|
||||
"name_useredit":"false",
|
||||
"OriginalName":"Entertainment Center",
|
||||
"Revoked":"false",
|
||||
"SmartPlugModel":"TP-Link Kasa KP115",
|
||||
"SSIEnabled":"true",
|
||||
"SSIModel":"SelfReporting",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"UserControlLock":"true",
|
||||
"UserDeletable":"false",
|
||||
"UserDeviceTypeDisplayString":"Smart Plug",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"false",
|
||||
"UserShowInDeviceList":"true",
|
||||
"UserVisibleDeviceId":"D8:44:89:30:93:F6"
|
||||
},
|
||||
"history":[
|
||||
0.010637,
|
||||
0.010685001,
|
||||
0.01068,
|
||||
0.010636,
|
||||
0.010737,
|
||||
0.010778,
|
||||
0.010765,
|
||||
0.010706999,
|
||||
0.04493,
|
||||
0.151627,
|
||||
0.081801005,
|
||||
0.14708,
|
||||
0.14865199,
|
||||
0.148901,
|
||||
0.099185996,
|
||||
0.010794,
|
||||
0.010876001,
|
||||
0.010772,
|
||||
0.010753999,
|
||||
0.010878,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.040078163,
|
||||
"total_kwh":0.96187586,
|
||||
"total_cost":17,
|
||||
"pct":5.1,
|
||||
"cost_history":[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"always_on",
|
||||
"name":"Always On",
|
||||
"icon":"alwayson",
|
||||
"tags":{
|
||||
"DefaultUserDeviceType":"AlwaysOn",
|
||||
"DeviceListAllowed":"true",
|
||||
"TimelineAllowed":"false",
|
||||
"UserDeleted":"false",
|
||||
"UserDeviceTypeDisplayString":"Always On",
|
||||
"UserEditable":"false",
|
||||
"UserMergeable":"false"
|
||||
},
|
||||
"history":[
|
||||
0.402,
|
||||
0.403366,
|
||||
0.405,
|
||||
0.4053,
|
||||
0.425181,
|
||||
0.443,
|
||||
0.444666,
|
||||
0.440596,
|
||||
0.40559798,
|
||||
0.404,
|
||||
0.404,
|
||||
0.404,
|
||||
0.404,
|
||||
0.404,
|
||||
0.4007,
|
||||
0.385716,
|
||||
0.388228,
|
||||
0.385812,
|
||||
0.38265,
|
||||
0.382,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.3383255,
|
||||
"total_kwh":8.119812,
|
||||
"total_cost":164,
|
||||
"pct":43.0,
|
||||
"cost_history":[
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
9,
|
||||
9,
|
||||
9,
|
||||
9,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"pZCoCHcc",
|
||||
"name":"Garage Freezer",
|
||||
"icon":"plug",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"ControlCapabilities":[
|
||||
"OnOff",
|
||||
"StandbyThreshold"
|
||||
],
|
||||
"DateCreated":"2024-08-22T19:23:30.000Z",
|
||||
"DefaultUserDeviceType":"SmartPlug",
|
||||
"DeviceListAllowed":"true",
|
||||
"DUID":"D8:44:89:30:81:D9",
|
||||
"IntegratedDeviceType":"IntegratedSmartPlug",
|
||||
"IntegrationType":"TPLink",
|
||||
"name_useredit":"false",
|
||||
"OriginalName":"Garage Freezer",
|
||||
"Revoked":"false",
|
||||
"SmartPlugModel":"TP-Link Kasa KP115",
|
||||
"SSIEnabled":"true",
|
||||
"SSIModel":"SelfReporting",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"UserControlLock":"true",
|
||||
"UserDeletable":"false",
|
||||
"UserDeviceTypeDisplayString":"Smart Plug",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"false",
|
||||
"UserShowInDeviceList":"true",
|
||||
"UserVisibleDeviceId":"D8:44:89:30:81:D9"
|
||||
},
|
||||
"history":[
|
||||
0.040856,
|
||||
0.032984,
|
||||
0.040568,
|
||||
0.032936,
|
||||
0.032405,
|
||||
0.038964,
|
||||
0.038643003,
|
||||
0.032064,
|
||||
0.038653,
|
||||
0.038432,
|
||||
0.031378,
|
||||
0.038960997,
|
||||
0.039219003,
|
||||
0.03901,
|
||||
0.039935,
|
||||
0.042015,
|
||||
0.041991003,
|
||||
0.047634996,
|
||||
0.036985002,
|
||||
0.047472,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.032129414,
|
||||
"total_kwh":0.77110595,
|
||||
"total_cost":20,
|
||||
"pct":4.1,
|
||||
"cost_history":[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"a6b4aa4d",
|
||||
"name":"Backyard Spotlight",
|
||||
"icon":"lightbulb",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"AlwaysOn":"false",
|
||||
"DateCreated":"2024-06-07T12:10:01.000Z",
|
||||
"DateFirstUsage":"2024-04-06",
|
||||
"DefaultUserDeviceType":"Light",
|
||||
"DeployToMonitor":"true",
|
||||
"DeviceListAllowed":"true",
|
||||
"ModelCreatedVersion":"24",
|
||||
"ModelUpdatedVersion":"29",
|
||||
"name_useredit":"true",
|
||||
"OriginalName":"Light 1",
|
||||
"PeerNames":[
|
||||
{
|
||||
"Name":"Light",
|
||||
"UserDeviceType":"Light",
|
||||
"Percent":91.0,
|
||||
"Icon":"lightbulb",
|
||||
"UserDeviceTypeDisplayString":"Light"
|
||||
},
|
||||
{
|
||||
"Name":"Appliance Light",
|
||||
"UserDeviceType":"ApplianceLight",
|
||||
"Percent":3.0,
|
||||
"Icon":"lightbulb",
|
||||
"UserDeviceTypeDisplayString":"Appliance Light"
|
||||
},
|
||||
{
|
||||
"Name":"TV/Monitor",
|
||||
"UserDeviceType":"TV",
|
||||
"Percent":3.0,
|
||||
"Icon":"tv",
|
||||
"UserDeviceTypeDisplayString":"TV/Monitor"
|
||||
},
|
||||
{
|
||||
"Name":"Microwave",
|
||||
"UserDeviceType":"Microwave",
|
||||
"Percent":2.0,
|
||||
"Icon":"microwave",
|
||||
"UserDeviceTypeDisplayString":"Microwave"
|
||||
}
|
||||
],
|
||||
"Pending":"false",
|
||||
"Revoked":"false",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"Type":"Lighting",
|
||||
"UserDeletable":"true",
|
||||
"UserDeviceTypeDisplayString":"Light",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"true",
|
||||
"UserShowInDeviceList":"true"
|
||||
},
|
||||
"history":[
|
||||
0.0,
|
||||
0.0,
|
||||
0.015547,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":6.4779166E-4,
|
||||
"total_kwh":0.015547001,
|
||||
"total_cost":0,
|
||||
"pct":0.1,
|
||||
"cost_history":[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"Pgz8OdRP",
|
||||
"name":"Oven",
|
||||
"icon":"stove",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"AlwaysOn":"false",
|
||||
"DateCreated":"2024-05-09T04:16:08.000Z",
|
||||
"DateFirstUsage":"2024-04-02",
|
||||
"DefaultUserDeviceType":"MysteryHeat",
|
||||
"DeployToMonitor":"true",
|
||||
"DeviceListAllowed":"true",
|
||||
"MergedDevices":"0988f51e,fc5b120b",
|
||||
"ModelCreatedVersion":"21",
|
||||
"name_useredit":"true",
|
||||
"OriginalName":"Heat 3",
|
||||
"PeerNames":[
|
||||
|
||||
],
|
||||
"Pending":"false",
|
||||
"Revoked":"false",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"Type":"UnknownHeat",
|
||||
"UserDeletable":"true",
|
||||
"UserDeviceType":"Oven",
|
||||
"UserDeviceTypeDisplayString":"Oven",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"true",
|
||||
"UserShowInDeviceList":"true",
|
||||
"Virtual":"true"
|
||||
},
|
||||
"make":"Decor",
|
||||
"history":[
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0017309999,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":7.2124996E-5,
|
||||
"total_kwh":0.0017309999,
|
||||
"total_cost":0,
|
||||
"pct":0.0,
|
||||
"cost_history":[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"given_make":"Decor"
|
||||
},
|
||||
{
|
||||
"id":"unknown",
|
||||
"name":"Unknown",
|
||||
"icon":"home",
|
||||
"monitor_id":869850,
|
||||
"tags":{
|
||||
|
||||
},
|
||||
"history":[
|
||||
0.35389698,
|
||||
0.37315995,
|
||||
0.367549,
|
||||
0.32328606,
|
||||
0.057057012,
|
||||
0.04399902,
|
||||
0.027577966,
|
||||
0.04153996,
|
||||
0.050042972,
|
||||
0.08149502,
|
||||
1.5312228,
|
||||
0.6084549,
|
||||
0.60922396,
|
||||
0.61388,
|
||||
0.74456716,
|
||||
0.045534983,
|
||||
0.046192013,
|
||||
0.06589394,
|
||||
0.06473405,
|
||||
0.29704997,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.2644316,
|
||||
"total_kwh":6.3463583,
|
||||
"total_cost":133,
|
||||
"pct":33.6,
|
||||
"cost_history":[
|
||||
7,
|
||||
8,
|
||||
8,
|
||||
7,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
32,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
15,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"total_cost":392,
|
||||
"total_costs":[
|
||||
25,
|
||||
17,
|
||||
18,
|
||||
24,
|
||||
11,
|
||||
12,
|
||||
17,
|
||||
11,
|
||||
12,
|
||||
18,
|
||||
46,
|
||||
25,
|
||||
26,
|
||||
31,
|
||||
27,
|
||||
10,
|
||||
17,
|
||||
11,
|
||||
11,
|
||||
23,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"production":{
|
||||
"total":42.46737,
|
||||
"totals":[
|
||||
-0.007275,
|
||||
-0.005593,
|
||||
-0.003241,
|
||||
-0.003823,
|
||||
-0.003187,
|
||||
-0.003171,
|
||||
0.016122999,
|
||||
0.334525,
|
||||
1.1068189,
|
||||
4.277293,
|
||||
5.843486,
|
||||
6.346029,
|
||||
6.397974,
|
||||
6.078468,
|
||||
5.257767,
|
||||
3.728918,
|
||||
2.0833168,
|
||||
0.840636,
|
||||
0.191979,
|
||||
-0.009675,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"devices":[
|
||||
{
|
||||
"id":"solar",
|
||||
"name":"Solar",
|
||||
"icon":"solar_alt",
|
||||
"tags":{
|
||||
"DefaultUserDeviceType":"Solar",
|
||||
"DeviceListAllowed":"false",
|
||||
"TimelineAllowed":"false",
|
||||
"UserDeleted":"false",
|
||||
"UserDeviceTypeDisplayString":"Solar",
|
||||
"UserEditable":"false",
|
||||
"UserMergeable":"false"
|
||||
},
|
||||
"history":[
|
||||
-0.007275,
|
||||
-0.005593,
|
||||
-0.003241,
|
||||
-0.003823,
|
||||
-0.003187,
|
||||
-0.003171,
|
||||
0.016122999,
|
||||
0.334525,
|
||||
1.1068189,
|
||||
4.277293,
|
||||
5.843486,
|
||||
6.346029,
|
||||
6.397974,
|
||||
6.078468,
|
||||
5.257767,
|
||||
3.728918,
|
||||
2.0833168,
|
||||
0.840636,
|
||||
0.191979,
|
||||
-0.009675,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":1.7694739,
|
||||
"total_cost":881,
|
||||
"pct":100.0,
|
||||
"cost_history":[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
7,
|
||||
23,
|
||||
89,
|
||||
121,
|
||||
132,
|
||||
133,
|
||||
126,
|
||||
109,
|
||||
77,
|
||||
43,
|
||||
17,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"total_cost":881,
|
||||
"total_costs":[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
7,
|
||||
23,
|
||||
89,
|
||||
121,
|
||||
132,
|
||||
133,
|
||||
126,
|
||||
109,
|
||||
77,
|
||||
43,
|
||||
17,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"grid_to_battery":null,
|
||||
"solar_to_home":null,
|
||||
"solar_to_battery":null,
|
||||
"battery_to_home":null,
|
||||
"battery_to_grid":null,
|
||||
"top_movers":null,
|
||||
"to_grid":31.2344913482666,
|
||||
"from_grid":7.588598728179932,
|
||||
"consumption_cost_change_cents":null,
|
||||
"consumption_percent_change":null,
|
||||
"production_percent_change":null,
|
||||
"to_grid_cost":648,
|
||||
"from_grid_cost":157,
|
||||
"trend_text":null,
|
||||
"usage_text":null,
|
||||
"trend_consumption":null,
|
||||
"trend_cost":null,
|
||||
"scale":"day",
|
||||
"solar_powered":60,
|
||||
"net_production":23.605871,
|
||||
"production_pct":225,
|
||||
"consumption_kwh_change":null
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
// SenseEnergyGetTrendsDevices
|
||||
|
||||
"devices":[
|
||||
{
|
||||
"id":"LpzF0vGG",
|
||||
"name":"Wine Cooler",
|
||||
"icon":"ac",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"AlwaysOn":"false",
|
||||
"DateCreated":"2024-04-11T09:28:45.000Z",
|
||||
"DateFirstUsage":"2024-04-01",
|
||||
"DefaultUserDeviceType":"AC",
|
||||
"DeployToMonitor":"true",
|
||||
"DeviceListAllowed":"true",
|
||||
"MergedDevices":"db3fc2f4,e329c895",
|
||||
"ModelCreatedVersion":"12",
|
||||
"ModelUpdatedVersion":"28",
|
||||
"name_useredit":"true",
|
||||
"NameUserGuess":"false",
|
||||
"OriginalName":"AC",
|
||||
"PeerNames":[
|
||||
|
||||
],
|
||||
"Pending":"false",
|
||||
"Revoked":"false",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"Type":"WindowAC",
|
||||
"UserDeletable":"true",
|
||||
"UserDeviceType":"AC",
|
||||
"UserDeviceTypeDisplayString":"AC",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"true",
|
||||
"UserShowBubble":"true",
|
||||
"UserShowInDeviceList":"true",
|
||||
"Virtual":"true"
|
||||
},
|
||||
"history":[
|
||||
0.37245202,
|
||||
0.01909,
|
||||
0.020947,
|
||||
0.368776,
|
||||
0.019066999,
|
||||
0.025436,
|
||||
0.28254902,
|
||||
0.019898001,
|
||||
0.019055,
|
||||
0.170864,
|
||||
0.14370401,
|
||||
0.019107,
|
||||
0.059858,
|
||||
0.296629,
|
||||
0.019107,
|
||||
0.019064,
|
||||
0.345176,
|
||||
0.018984,
|
||||
0.040691003,
|
||||
0.364613,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.110211134,
|
||||
"total_kwh":2.6450672,
|
||||
"total_cost":53,
|
||||
"pct":14.0,
|
||||
"cost_history":[
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
3,
|
||||
0,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
7,
|
||||
0,
|
||||
1,
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}]
|
|
@ -0,0 +1,802 @@
|
|||
// SenseEnergyApiGetTrendsTotal
|
||||
|
||||
"consumption":{
|
||||
"total":18.861498,
|
||||
"totals":[
|
||||
1.179842,
|
||||
0.83928496,
|
||||
0.860291,
|
||||
1.140934,
|
||||
0.544447,
|
||||
0.562177,
|
||||
0.804201,
|
||||
0.544805,
|
||||
0.558279,
|
||||
0.846418,
|
||||
2.193837,
|
||||
1.217603,
|
||||
1.260953,
|
||||
1.5024201,
|
||||
1.303495,
|
||||
0.503124,
|
||||
0.832463,
|
||||
0.52909696,
|
||||
0.53581405,
|
||||
1.102013,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"devices":[
|
||||
{
|
||||
"id":"LpzF0vGG",
|
||||
"name":"Wine Cooler",
|
||||
"icon":"ac",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"AlwaysOn":"false",
|
||||
"DateCreated":"2024-04-11T09:28:45.000Z",
|
||||
"DateFirstUsage":"2024-04-01",
|
||||
"DefaultUserDeviceType":"AC",
|
||||
"DeployToMonitor":"true",
|
||||
"DeviceListAllowed":"true",
|
||||
"MergedDevices":"db3fc2f4,e329c895",
|
||||
"ModelCreatedVersion":"12",
|
||||
"ModelUpdatedVersion":"28",
|
||||
"name_useredit":"true",
|
||||
"NameUserGuess":"false",
|
||||
"OriginalName":"AC",
|
||||
"PeerNames":[
|
||||
|
||||
],
|
||||
"Pending":"false",
|
||||
"Revoked":"false",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"Type":"WindowAC",
|
||||
"UserDeletable":"true",
|
||||
"UserDeviceType":"AC",
|
||||
"UserDeviceTypeDisplayString":"AC",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"true",
|
||||
"UserShowBubble":"true",
|
||||
"UserShowInDeviceList":"true",
|
||||
"Virtual":"true"
|
||||
},
|
||||
"history":[
|
||||
0.37245202,
|
||||
0.01909,
|
||||
0.020947,
|
||||
0.368776,
|
||||
0.019066999,
|
||||
0.025436,
|
||||
0.28254902,
|
||||
0.019898001,
|
||||
0.019055,
|
||||
0.170864,
|
||||
0.14370401,
|
||||
0.019107,
|
||||
0.059858,
|
||||
0.296629,
|
||||
0.019107,
|
||||
0.019064,
|
||||
0.345176,
|
||||
0.018984,
|
||||
0.040691003,
|
||||
0.364613,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.110211134,
|
||||
"total_kwh":2.6450672,
|
||||
"total_cost":53,
|
||||
"pct":14.0,
|
||||
"cost_history":[
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
3,
|
||||
0,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
7,
|
||||
0,
|
||||
1,
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"wIpu0Jjw",
|
||||
"name":"Entertainment Center",
|
||||
"icon":"plug",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"ControlCapabilities":[
|
||||
"OnOff",
|
||||
"StandbyThreshold"
|
||||
],
|
||||
"DateCreated":"2024-08-22T19:23:30.000Z",
|
||||
"DefaultUserDeviceType":"SmartPlug",
|
||||
"DeviceListAllowed":"true",
|
||||
"DUID":"D8:44:89:30:93:F6",
|
||||
"IntegratedDeviceType":"IntegratedSmartPlug",
|
||||
"IntegrationType":"TPLink",
|
||||
"name_useredit":"false",
|
||||
"OriginalName":"Entertainment Center",
|
||||
"Revoked":"false",
|
||||
"SmartPlugModel":"TP-Link Kasa KP115",
|
||||
"SSIEnabled":"true",
|
||||
"SSIModel":"SelfReporting",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"UserControlLock":"true",
|
||||
"UserDeletable":"false",
|
||||
"UserDeviceTypeDisplayString":"Smart Plug",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"false",
|
||||
"UserShowInDeviceList":"true",
|
||||
"UserVisibleDeviceId":"D8:44:89:30:93:F6"
|
||||
},
|
||||
"history":[
|
||||
0.010637,
|
||||
0.010685001,
|
||||
0.01068,
|
||||
0.010636,
|
||||
0.010737,
|
||||
0.010778,
|
||||
0.010765,
|
||||
0.010706999,
|
||||
0.04493,
|
||||
0.151627,
|
||||
0.081801005,
|
||||
0.14708,
|
||||
0.14865199,
|
||||
0.148901,
|
||||
0.099185996,
|
||||
0.010794,
|
||||
0.010876001,
|
||||
0.010772,
|
||||
0.010753999,
|
||||
0.010878,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.040078163,
|
||||
"total_kwh":0.96187586,
|
||||
"total_cost":17,
|
||||
"pct":5.1,
|
||||
"cost_history":[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"always_on",
|
||||
"name":"Always On",
|
||||
"icon":"alwayson",
|
||||
"tags":{
|
||||
"DefaultUserDeviceType":"AlwaysOn",
|
||||
"DeviceListAllowed":"true",
|
||||
"TimelineAllowed":"false",
|
||||
"UserDeleted":"false",
|
||||
"UserDeviceTypeDisplayString":"Always On",
|
||||
"UserEditable":"false",
|
||||
"UserMergeable":"false"
|
||||
},
|
||||
"history":[
|
||||
0.402,
|
||||
0.403366,
|
||||
0.405,
|
||||
0.4053,
|
||||
0.425181,
|
||||
0.443,
|
||||
0.444666,
|
||||
0.440596,
|
||||
0.40559798,
|
||||
0.404,
|
||||
0.404,
|
||||
0.404,
|
||||
0.404,
|
||||
0.404,
|
||||
0.4007,
|
||||
0.385716,
|
||||
0.388228,
|
||||
0.385812,
|
||||
0.38265,
|
||||
0.382,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.3383255,
|
||||
"total_kwh":8.119812,
|
||||
"total_cost":164,
|
||||
"pct":43.0,
|
||||
"cost_history":[
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
9,
|
||||
9,
|
||||
9,
|
||||
9,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"pZCoCHcc",
|
||||
"name":"Garage Freezer",
|
||||
"icon":"plug",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"ControlCapabilities":[
|
||||
"OnOff",
|
||||
"StandbyThreshold"
|
||||
],
|
||||
"DateCreated":"2024-08-22T19:23:30.000Z",
|
||||
"DefaultUserDeviceType":"SmartPlug",
|
||||
"DeviceListAllowed":"true",
|
||||
"DUID":"D8:44:89:30:81:D9",
|
||||
"IntegratedDeviceType":"IntegratedSmartPlug",
|
||||
"IntegrationType":"TPLink",
|
||||
"name_useredit":"false",
|
||||
"OriginalName":"Garage Freezer",
|
||||
"Revoked":"false",
|
||||
"SmartPlugModel":"TP-Link Kasa KP115",
|
||||
"SSIEnabled":"true",
|
||||
"SSIModel":"SelfReporting",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"UserControlLock":"true",
|
||||
"UserDeletable":"false",
|
||||
"UserDeviceTypeDisplayString":"Smart Plug",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"false",
|
||||
"UserShowInDeviceList":"true",
|
||||
"UserVisibleDeviceId":"D8:44:89:30:81:D9"
|
||||
},
|
||||
"history":[
|
||||
0.040856,
|
||||
0.032984,
|
||||
0.040568,
|
||||
0.032936,
|
||||
0.032405,
|
||||
0.038964,
|
||||
0.038643003,
|
||||
0.032064,
|
||||
0.038653,
|
||||
0.038432,
|
||||
0.031378,
|
||||
0.038960997,
|
||||
0.039219003,
|
||||
0.03901,
|
||||
0.039935,
|
||||
0.042015,
|
||||
0.041991003,
|
||||
0.047634996,
|
||||
0.036985002,
|
||||
0.047472,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.032129414,
|
||||
"total_kwh":0.77110595,
|
||||
"total_cost":20,
|
||||
"pct":4.1,
|
||||
"cost_history":[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"a6b4aa4d",
|
||||
"name":"Backyard Spotlight",
|
||||
"icon":"lightbulb",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"AlwaysOn":"false",
|
||||
"DateCreated":"2024-06-07T12:10:01.000Z",
|
||||
"DateFirstUsage":"2024-04-06",
|
||||
"DefaultUserDeviceType":"Light",
|
||||
"DeployToMonitor":"true",
|
||||
"DeviceListAllowed":"true",
|
||||
"ModelCreatedVersion":"24",
|
||||
"ModelUpdatedVersion":"29",
|
||||
"name_useredit":"true",
|
||||
"OriginalName":"Light 1",
|
||||
"PeerNames":[
|
||||
{
|
||||
"Name":"Light",
|
||||
"UserDeviceType":"Light",
|
||||
"Percent":91.0,
|
||||
"Icon":"lightbulb",
|
||||
"UserDeviceTypeDisplayString":"Light"
|
||||
},
|
||||
{
|
||||
"Name":"Appliance Light",
|
||||
"UserDeviceType":"ApplianceLight",
|
||||
"Percent":3.0,
|
||||
"Icon":"lightbulb",
|
||||
"UserDeviceTypeDisplayString":"Appliance Light"
|
||||
},
|
||||
{
|
||||
"Name":"TV/Monitor",
|
||||
"UserDeviceType":"TV",
|
||||
"Percent":3.0,
|
||||
"Icon":"tv",
|
||||
"UserDeviceTypeDisplayString":"TV/Monitor"
|
||||
},
|
||||
{
|
||||
"Name":"Microwave",
|
||||
"UserDeviceType":"Microwave",
|
||||
"Percent":2.0,
|
||||
"Icon":"microwave",
|
||||
"UserDeviceTypeDisplayString":"Microwave"
|
||||
}
|
||||
],
|
||||
"Pending":"false",
|
||||
"Revoked":"false",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"Type":"Lighting",
|
||||
"UserDeletable":"true",
|
||||
"UserDeviceTypeDisplayString":"Light",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"true",
|
||||
"UserShowInDeviceList":"true"
|
||||
},
|
||||
"history":[
|
||||
0.0,
|
||||
0.0,
|
||||
0.015547,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":6.4779166E-4,
|
||||
"total_kwh":0.015547001,
|
||||
"total_cost":0,
|
||||
"pct":0.1,
|
||||
"cost_history":[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"Pgz8OdRP",
|
||||
"name":"Oven",
|
||||
"icon":"stove",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"AlwaysOn":"false",
|
||||
"DateCreated":"2024-05-09T04:16:08.000Z",
|
||||
"DateFirstUsage":"2024-04-02",
|
||||
"DefaultUserDeviceType":"MysteryHeat",
|
||||
"DeployToMonitor":"true",
|
||||
"DeviceListAllowed":"true",
|
||||
"MergedDevices":"0988f51e,fc5b120b",
|
||||
"ModelCreatedVersion":"21",
|
||||
"name_useredit":"true",
|
||||
"OriginalName":"Heat 3",
|
||||
"PeerNames":[
|
||||
|
||||
],
|
||||
"Pending":"false",
|
||||
"Revoked":"false",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"Type":"UnknownHeat",
|
||||
"UserDeletable":"true",
|
||||
"UserDeviceType":"Oven",
|
||||
"UserDeviceTypeDisplayString":"Oven",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"true",
|
||||
"UserShowInDeviceList":"true",
|
||||
"Virtual":"true"
|
||||
},
|
||||
"make":"Decor",
|
||||
"history":[
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0017309999,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":7.2124996E-5,
|
||||
"total_kwh":0.0017309999,
|
||||
"total_cost":0,
|
||||
"pct":0.0,
|
||||
"cost_history":[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"given_make":"Decor"
|
||||
},
|
||||
{
|
||||
"id":"unknown",
|
||||
"name":"Unknown",
|
||||
"icon":"home",
|
||||
"monitor_id":869850,
|
||||
"tags":{
|
||||
|
||||
},
|
||||
"history":[
|
||||
0.35389698,
|
||||
0.37315995,
|
||||
0.367549,
|
||||
0.32328606,
|
||||
0.057057012,
|
||||
0.04399902,
|
||||
0.027577966,
|
||||
0.04153996,
|
||||
0.050042972,
|
||||
0.08149502,
|
||||
1.5312228,
|
||||
0.6084549,
|
||||
0.60922396,
|
||||
0.61388,
|
||||
0.74456716,
|
||||
0.045534983,
|
||||
0.046192013,
|
||||
0.06589394,
|
||||
0.06473405,
|
||||
0.29704997,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":0.2644316,
|
||||
"total_kwh":6.3463583,
|
||||
"total_cost":133,
|
||||
"pct":33.6,
|
||||
"cost_history":[
|
||||
7,
|
||||
8,
|
||||
8,
|
||||
7,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
32,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
15,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"total_cost":392,
|
||||
"total_costs":[
|
||||
25,
|
||||
17,
|
||||
18,
|
||||
24,
|
||||
11,
|
||||
12,
|
||||
17,
|
||||
11,
|
||||
12,
|
||||
18,
|
||||
46,
|
||||
25,
|
||||
26,
|
||||
31,
|
||||
27,
|
||||
10,
|
||||
17,
|
||||
11,
|
||||
11,
|
||||
23,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"production":{
|
||||
"total":42.46737,
|
||||
"totals":[
|
||||
-0.007275,
|
||||
-0.005593,
|
||||
-0.003241,
|
||||
-0.003823,
|
||||
-0.003187,
|
||||
-0.003171,
|
||||
0.016122999,
|
||||
0.334525,
|
||||
1.1068189,
|
||||
4.277293,
|
||||
5.843486,
|
||||
6.346029,
|
||||
6.397974,
|
||||
6.078468,
|
||||
5.257767,
|
||||
3.728918,
|
||||
2.0833168,
|
||||
0.840636,
|
||||
0.191979,
|
||||
-0.009675,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"devices":[
|
||||
{
|
||||
"id":"solar",
|
||||
"name":"Solar",
|
||||
"icon":"solar_alt",
|
||||
"tags":{
|
||||
"DefaultUserDeviceType":"Solar",
|
||||
"DeviceListAllowed":"false",
|
||||
"TimelineAllowed":"false",
|
||||
"UserDeleted":"false",
|
||||
"UserDeviceTypeDisplayString":"Solar",
|
||||
"UserEditable":"false",
|
||||
"UserMergeable":"false"
|
||||
},
|
||||
"history":[
|
||||
-0.007275,
|
||||
-0.005593,
|
||||
-0.003241,
|
||||
-0.003823,
|
||||
-0.003187,
|
||||
-0.003171,
|
||||
0.016122999,
|
||||
0.334525,
|
||||
1.1068189,
|
||||
4.277293,
|
||||
5.843486,
|
||||
6.346029,
|
||||
6.397974,
|
||||
6.078468,
|
||||
5.257767,
|
||||
3.728918,
|
||||
2.0833168,
|
||||
0.840636,
|
||||
0.191979,
|
||||
-0.009675,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"avgw":1.7694739,
|
||||
"total_cost":881,
|
||||
"pct":100.0,
|
||||
"cost_history":[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
7,
|
||||
23,
|
||||
89,
|
||||
121,
|
||||
132,
|
||||
133,
|
||||
126,
|
||||
109,
|
||||
77,
|
||||
43,
|
||||
17,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"total_cost":881,
|
||||
"total_costs":[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
7,
|
||||
23,
|
||||
89,
|
||||
121,
|
||||
132,
|
||||
133,
|
||||
126,
|
||||
109,
|
||||
77,
|
||||
43,
|
||||
17,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// SenseEnergyApiMonitor
|
||||
|
||||
{
|
||||
"checksum": "5E4E0A223C1A8641BF2BF7473CB9F7EAB7B8ED7D",
|
||||
"device_data_checksum": "534D88564331E1709A907AD234D9FD3E9DEB3876",
|
||||
"monitor_overview": {
|
||||
"monitor": {
|
||||
"id": xxxx,
|
||||
"date_created": "2024-04-01T17:12:45.000Z",
|
||||
"serial_number": "N327004101",
|
||||
"time_zone": "America/Los_Angeles",
|
||||
"solar_connected": true,
|
||||
"solar_configured": true,
|
||||
"attributes": {
|
||||
"id": 117968,
|
||||
"name": "",
|
||||
"state": "CA",
|
||||
"cost": 20.77,
|
||||
"sell_back_rate": 20.77,
|
||||
"user_set_cost": false,
|
||||
"cycle_start": null,
|
||||
"basement_type": "No basement",
|
||||
"home_size_type": "3250 sq. ft",
|
||||
"home_type": "Single family",
|
||||
"number_of_occupants": "3",
|
||||
"occupancy_type": "Full-time",
|
||||
"year_built_type": "1980s",
|
||||
"basement_type_key": "basement_type_no_basement",
|
||||
"home_size_type_key": "home_size_3250sqft",
|
||||
"home_type_key": "home_type_single_family",
|
||||
"occupancy_type_key": "occupancy_full_time",
|
||||
"year_built_type_key": "year_built_1980s",
|
||||
"address": null,
|
||||
"city": null,
|
||||
"postal_code": "xxxx",
|
||||
"electricity_cost": {
|
||||
"id": 6,
|
||||
"location": "California",
|
||||
"abbreviation": "CA",
|
||||
"cost": 20.77,
|
||||
"national_electricity_cost": {
|
||||
"id": 1,
|
||||
"location": "United States",
|
||||
"abbreviation": "US",
|
||||
"cost": 13.31
|
||||
},
|
||||
"national_electricity_cost_id": 1
|
||||
},
|
||||
"show_cost": true,
|
||||
"tou_enabled": false,
|
||||
"solar_tou_enabled": false,
|
||||
"power_region": "CAISO",
|
||||
"to_grid_threshold": null,
|
||||
"panel": null,
|
||||
"home_info_survey_progress": "COMPLETED",
|
||||
"device_survey_progress": "COMPLETED",
|
||||
"user_set_sell_back_rate": true
|
||||
},
|
||||
"signal_check_completed_time": "2024-04-01T17:31:55.000Z",
|
||||
"data_sharing": [],
|
||||
"ethernet_supported": false,
|
||||
"power_over_ethernet_supported": false,
|
||||
"aux_ignore": false,
|
||||
"aux_port": "solar",
|
||||
"hardware_type": "monitor",
|
||||
"zigbee_supported": false
|
||||
},
|
||||
"ndi_enabled": false,
|
||||
"local_api_enabled": false,
|
||||
"partner_channel": "6-4",
|
||||
"partner_tags": [
|
||||
"CHANNEL__Amazon"
|
||||
],
|
||||
"num_devices": 15,
|
||||
"num_named_devices": 14,
|
||||
"num_unnamed_devices": 1
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SenseEnergyMonitorInfo
|
||||
|
||||
"monitor_info": {
|
||||
"emac":"f2:ed:5e:b1:96:8e",
|
||||
"wifi_strength":86,
|
||||
"ip_address":"192.168.1.36",
|
||||
"connection_state":"ONLINE",
|
||||
"version":"1.49.29-4a5ddd65-release",
|
||||
"ssid":"jameswireless_EXT",
|
||||
"mac":"24:cd:8d:3d:dd:79",
|
||||
"ethernet":false,
|
||||
"test_result":"Good as of 04/01/2024",
|
||||
"serial":"N327004101",
|
||||
"ndt_enabled":false,
|
||||
"online":true,
|
||||
"signal":null
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// SenseEnergyApiMonitorStatus
|
||||
|
||||
{ "signals": {
|
||||
"progress":100,
|
||||
"status":"OK"
|
||||
},
|
||||
"device_detection": {
|
||||
"in_progress":[],
|
||||
"found":[],
|
||||
"num_detected":15
|
||||
},
|
||||
"monitor_info": {
|
||||
"emac":"f2:ed:5e:b1:96:8e",
|
||||
"wifi_strength":86,
|
||||
"ip_address":"192.168.1.36",
|
||||
"connection_state":"ONLINE",
|
||||
"version":"1.49.29-4a5ddd65-release",
|
||||
"ssid":"jameswireless_EXT",
|
||||
"mac":"24:cd:8d:3d:dd:79",
|
||||
"ethernet":false,
|
||||
"test_result":"Good as of 04/01/2024",
|
||||
"serial":"N327004101",
|
||||
"ndt_enabled":false,
|
||||
"online":true,
|
||||
"signal":null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// SenseEnergyApiRefreshToken
|
||||
|
||||
{
|
||||
"authorized": true,
|
||||
"account_id": 139132,
|
||||
"user_id": 135624,
|
||||
"access_token": "t1.v2.eyJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJTZW5zZSBJbmMiLCJleHAiOjE3MjYwMTQwNzYsInVzZXJJZCI6MTM1NjI0LCJhY2NvdW50SWQiOjEzOTEzMiwicm9sZXMiOiJ1c2VyIiwiZGhhc2giOiJhZDkyMSJ9.e6cBHBJl4k4EtG5kLPmxjOltbEn9_ulrdkL8ktU_OlNhdHoQtqG9HsPKivTgZcY-0b9GG_Pl_HeYNIeC5ikFfw",
|
||||
"roles": "user",
|
||||
"refresh_token": "DB6pZEWbY2F1e4hP+G3z/uE2qt+dcEQxAcwUBNSHG5KDabGp",
|
||||
"totp_enabled": false,
|
||||
"expires": "2024-09-11T00:21:16.805Z"
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// SenseEnergyDatagramGetRealtime
|
||||
|
||||
{
|
||||
"system": {
|
||||
"get_sysinfo": {
|
||||
"err_code": 0,
|
||||
"sw_ver": "1.2.5 Build 171206 Rel.085954",
|
||||
"hw_ver": "1.0",
|
||||
"type: "IOT.SMARTPLUGSWITCH",
|
||||
"model": "HS110(US)",
|
||||
"mac": "xxx",
|
||||
"deviceId": "xxx",
|
||||
"alias": "alias",
|
||||
"relay_state": 1,
|
||||
"updating": 0
|
||||
}
|
||||
},
|
||||
"emeter": {
|
||||
"get_realtime": {
|
||||
"current": x,
|
||||
"voltage": x,
|
||||
"power": x,
|
||||
"total": 0,
|
||||
"err_code": 0
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// SenseEnergyDatagramGetSysInfo
|
||||
|
||||
{
|
||||
"system": {
|
||||
"get_sysinfo": {
|
||||
"err_code": 0,
|
||||
"sw_ver": "1.2.5 Build 171206 Rel.085954",
|
||||
"hw_ver": "1.0",
|
||||
"type: "IOT.SMARTPLUGSWITCH",
|
||||
"model": "HS110(US)",
|
||||
"mac": "xxx",
|
||||
"deviceId": "xxx",
|
||||
"alias": "alias",
|
||||
"relay_state": 1,
|
||||
"updating": 0
|
||||
}
|
||||
},
|
||||
"emeter": {
|
||||
"get_realtime": {
|
||||
"current": x,
|
||||
"voltage": x,
|
||||
"power": x,
|
||||
"total": 0,
|
||||
"err_code": 0
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// SenseEnergyWebSocketDevice
|
||||
|
||||
{
|
||||
"id":"solar",
|
||||
"name":"Solar",
|
||||
"icon":"solar_alt",
|
||||
"tags":{
|
||||
"DefaultUserDeviceType":"Solar",
|
||||
"DeviceListAllowed":"false",
|
||||
"TimelineAllowed":"false",
|
||||
"UserDeleted":"false",
|
||||
"UserDeviceType":"Solar",
|
||||
"UserDeviceTypeDisplayString":"Solar",
|
||||
"UserEditable":"false",
|
||||
"UserMergeable":"false"
|
||||
},
|
||||
"attrs":[
|
||||
|
||||
],
|
||||
"w":6104.5957
|
||||
},
|
||||
{
|
||||
"id": "SgA3CQgg",
|
||||
"name": "Garage Freezer",
|
||||
"icon": "plug",
|
||||
"tags": {
|
||||
"Alertable": "true",
|
||||
"ControlCapabilities": [
|
||||
"OnOff",
|
||||
"StandbyThreshold"
|
||||
],
|
||||
"DateCreated": "2024-12-02T00:03:45.000Z",
|
||||
"DefaultUserDeviceType": "SmartPlug",
|
||||
"DeviceListAllowed": "true",
|
||||
"DUID": "D8:44:89:30:81:D9",
|
||||
"IntegratedDeviceType": "IntegratedSmartPlug",
|
||||
"IntegrationType": "TPLink",
|
||||
"name_useredit": "false",
|
||||
"OriginalName": "Garage Freezer",
|
||||
"Revoked": "false",
|
||||
"SmartPlugModel": "TP-Link Kasa KP115",
|
||||
"SSIEnabled": "true",
|
||||
"SSIModel": "SelfReporting",
|
||||
"TimelineAllowed": "true",
|
||||
"TimelineDefault": "true",
|
||||
"UserDeletable": "false",
|
||||
"UserDeviceType": "SmartPlug",
|
||||
"UserDeviceTypeDisplayString": "Smart Plug",
|
||||
"UserEditable": "true",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "false",
|
||||
"UserShowInDeviceList": "true",
|
||||
"UserVisibleDeviceId": "D8:44:89:30:81:D9"
|
||||
},
|
||||
"attrs": [],
|
||||
"w": 37.227,
|
||||
"sd": {
|
||||
"w": 37.227,
|
||||
"i": 0.574,
|
||||
"v": 121.507,
|
||||
"e": 4.396
|
||||
},
|
||||
"ao_w": 1.0,
|
||||
"ao_st": false
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// SenseEnergyWebSocketDeviceTags
|
||||
|
||||
"tags": {
|
||||
"DefaultUserDeviceType": "ElectricVehicle",
|
||||
"DeviceListAllowed": "true",
|
||||
"Stage": "Tracking",
|
||||
"UserAdded": "false",
|
||||
"UserDeviceType": "ElectricVehicle",
|
||||
"UserEditable": "false",
|
||||
"UserMergeable": "false"
|
||||
"Alertable": "false",
|
||||
"AlwaysOn": "false",
|
||||
"DateCreated": "2024-05-09T04:16:08.000Z",
|
||||
"DateFirstUsage": "2024-04-02",
|
||||
"DefaultUserDeviceType": "AC",
|
||||
"DeployToMonitor": "true",
|
||||
"DeviceListAllowed": "false",
|
||||
"ModelCreatedVersion": "21",
|
||||
"ModelUpdatedVersion": "28",
|
||||
"name_useredit": "true",
|
||||
"OriginalName": "AC 2",
|
||||
"Pending": "false",
|
||||
"Revoked": "false",
|
||||
"TimelineAllowed": "false",
|
||||
"TimelineDefault": "false",
|
||||
"Type": "AC",
|
||||
"UserDeletable": "true",
|
||||
"UserDeleted": "true",
|
||||
"UserDeviceType": "OtherDevice",
|
||||
"UserDeviceTypeDisplayString": "Other Device",
|
||||
"UserEditable": "false",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "true",
|
||||
"UserShowInDeviceList": "true"
|
||||
},
|
||||
{
|
||||
"id": "zDcE5VOr",
|
||||
"name": "Sense Whole House Proxy Fan",
|
||||
"icon": "plug",
|
||||
"tags": {
|
||||
"Alertable": "false",
|
||||
"ControlCapabilities": [
|
||||
"OnOff",
|
||||
"StandbyThreshold"
|
||||
],
|
||||
"DateCreated": "2024-09-22T03:13:45.000Z",
|
||||
"DefaultUserDeviceType": "SmartPlug",
|
||||
"DeviceListAllowed": "false",
|
||||
"DUID": "53:75:31:70:55:53",
|
||||
"IntegratedDeviceType": "IntegratedSmartPlug",
|
||||
"IntegrationType": "TPLink",
|
||||
"name_useredit": "false",
|
||||
"OriginalName": "Sense Whole House Proxy Fan",
|
||||
"Revoked": "false",
|
||||
"SmartPlugModel": "TP-Link Kasa HS110",
|
||||
"SSIEnabled": "true",
|
||||
"SSIModel": "SelfReporting",
|
||||
"TimelineAllowed": "false",
|
||||
"TimelineDefault": "false",
|
||||
"UserDeletable": "false",
|
||||
"UserDeleted": "true",
|
||||
"UserDeviceType": "SmartPlug",
|
||||
"UserDeviceTypeDisplayString": "Smart Plug",
|
||||
"UserEditable": "false",
|
||||
"UserEditableMeta": "true",
|
||||
"UserMergeable": "false",
|
||||
"UserShowInDeviceList": "true",
|
||||
"UserVisibleDeviceId": "53:75:31:70:55:53"
|
||||
}
|
||||
},
|
|
@ -0,0 +1,173 @@
|
|||
// SenseEnergyWebSocketRealtimeUpdate
|
||||
|
||||
{
|
||||
"payload":{
|
||||
"voltage":[
|
||||
121.23905944824219,
|
||||
121.71208190917969
|
||||
],
|
||||
"frame":53187540,
|
||||
"devices":[
|
||||
{
|
||||
"id":"solar",
|
||||
"name":"Solar",
|
||||
"icon":"solar_alt",
|
||||
"tags":{
|
||||
"DefaultUserDeviceType":"Solar",
|
||||
"DeviceListAllowed":"false",
|
||||
"TimelineAllowed":"false",
|
||||
"UserDeleted":"false",
|
||||
"UserDeviceType":"Solar",
|
||||
"UserDeviceTypeDisplayString":"Solar",
|
||||
"UserEditable":"false",
|
||||
"UserMergeable":"false"
|
||||
},
|
||||
"attrs":[
|
||||
|
||||
],
|
||||
"w":6104.5957
|
||||
},
|
||||
{
|
||||
"id":"unknown",
|
||||
"name":"Other",
|
||||
"icon":"home",
|
||||
"tags":{
|
||||
"DefaultUserDeviceType":"Unknown",
|
||||
"DeviceListAllowed":"true",
|
||||
"TimelineAllowed":"false",
|
||||
"UserDeleted":"false",
|
||||
"UserDeviceType":"Unknown",
|
||||
"UserDeviceTypeDisplayString":"Unknown",
|
||||
"UserEditable":"false",
|
||||
"UserMergeable":"false"
|
||||
},
|
||||
"attrs":[
|
||||
|
||||
],
|
||||
"w":550.27637
|
||||
},
|
||||
{
|
||||
"id":"0e64ad2b",
|
||||
"name":"AC 2",
|
||||
"icon":"ac",
|
||||
"tags":{
|
||||
"Alertable":"true",
|
||||
"AlwaysOn":"false",
|
||||
"DateCreated":"2024-05-09T04:16:08.000Z",
|
||||
"DateFirstUsage":"2024-04-02",
|
||||
"DefaultUserDeviceType":"AC",
|
||||
"DeployToMonitor":"true",
|
||||
"DeviceListAllowed":"true",
|
||||
"ModelCreatedVersion":"21",
|
||||
"ModelUpdatedVersion":"27",
|
||||
"name_useredit":"false",
|
||||
"OriginalName":"AC 2",
|
||||
"PeerNames":[
|
||||
{
|
||||
"Name":"Gas Dryer",
|
||||
"UserDeviceType":"GasDryer",
|
||||
"Percent":97.0,
|
||||
"Icon":"washer",
|
||||
"UserDeviceTypeDisplayString":"Gas Dryer"
|
||||
},
|
||||
{
|
||||
"Name":"Furnace",
|
||||
"UserDeviceType":"Furnace",
|
||||
"Percent":1.0,
|
||||
"Icon":"heat",
|
||||
"UserDeviceTypeDisplayString":"Furnace"
|
||||
},
|
||||
{
|
||||
"Name":"Pump",
|
||||
"UserDeviceType":"Pump",
|
||||
"Percent":1.0,
|
||||
"Icon":"pump",
|
||||
"UserDeviceTypeDisplayString":"Pump"
|
||||
},
|
||||
{
|
||||
"Name":"Washer",
|
||||
"UserDeviceType":"Washer",
|
||||
"Percent":1.0,
|
||||
"Icon":"washer",
|
||||
"UserDeviceTypeDisplayString":"Washer"
|
||||
}
|
||||
],
|
||||
"Pending":"false",
|
||||
"Revoked":"false",
|
||||
"TimelineAllowed":"true",
|
||||
"TimelineDefault":"true",
|
||||
"Type":"AC",
|
||||
"UserDeletable":"true",
|
||||
"UserDeviceType":"AC",
|
||||
"UserDeviceTypeDisplayString":"AC",
|
||||
"UserEditable":"true",
|
||||
"UserEditableMeta":"true",
|
||||
"UserMergeable":"true",
|
||||
"UserShowInDeviceList":"true"
|
||||
},
|
||||
"attrs":[
|
||||
|
||||
],
|
||||
"w":498.96848
|
||||
},
|
||||
{
|
||||
"id":"always_on",
|
||||
"name":"Always On",
|
||||
"icon":"alwayson",
|
||||
"tags":{
|
||||
"DefaultUserDeviceType":"AlwaysOn",
|
||||
"DeviceListAllowed":"true",
|
||||
"TimelineAllowed":"false",
|
||||
"UserDeleted":"false",
|
||||
"UserDeviceType":"AlwaysOn",
|
||||
"UserDeviceTypeDisplayString":"Always On",
|
||||
"UserEditable":"false",
|
||||
"UserMergeable":"false"
|
||||
},
|
||||
"attrs":[
|
||||
|
||||
],
|
||||
"w":445.0
|
||||
}
|
||||
],
|
||||
"deltas":[
|
||||
|
||||
],
|
||||
"defaultCost":20.77,
|
||||
"channels":[
|
||||
810.09912109375,
|
||||
684.145751953125
|
||||
],
|
||||
"hz":59.98676681518555,
|
||||
"w":1494.244873046875,
|
||||
"c":31,
|
||||
"solar_w":6104.595703125,
|
||||
"grid_w":-4610,
|
||||
"solar_c":126,
|
||||
"_stats":{
|
||||
"brcv":1.7236690510805538E9,
|
||||
"mrcv":1.723669051122E9,
|
||||
"msnd":1.723669051123E9
|
||||
},
|
||||
"aux":{
|
||||
"solar":[
|
||||
-3051.75732421875,
|
||||
-3052.838134765625
|
||||
]
|
||||
},
|
||||
"power_flow":{
|
||||
"solar":[
|
||||
"grid",
|
||||
"home"
|
||||
],
|
||||
"grid":[
|
||||
|
||||
]
|
||||
},
|
||||
"solar_pct":100,
|
||||
"d_w":1494,
|
||||
"d_solar_w":6105,
|
||||
"epoch":1723669048
|
||||
},
|
||||
"type":"realtime_update"
|
||||
})
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.internal.handler.helpers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
|
||||
/**
|
||||
* {@link SenseEnergyPowerLevesTest }
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class SenseEnergyPowerLevelsTest {
|
||||
SenseEnergyPowerLevels powerLevels = new SenseEnergyPowerLevels();
|
||||
|
||||
@SuppressWarnings("null")
|
||||
void testParse(String s, int itemsLevel, int itemsState, String pretty, @Nullable List<Object> tests) {
|
||||
QuantityType<Power> qtResult;
|
||||
float result;
|
||||
|
||||
powerLevels.parse(s);
|
||||
Assertions.assertEquals(itemsLevel, powerLevels.getNumValueLevels(), "Wrong number of Value Levels");
|
||||
Assertions.assertEquals(itemsState, powerLevels.getNumStateLevels(), "Wrong number of State Levels");
|
||||
|
||||
String levelsPretty = powerLevels.toString();
|
||||
Assertions.assertEquals(pretty, levelsPretty, "toString format differs");
|
||||
|
||||
if (tests == null) {
|
||||
return;
|
||||
}
|
||||
Iterator<Object> iter = tests.iterator();
|
||||
while (iter.hasNext()) {
|
||||
qtResult = null;
|
||||
Object value = iter.next();
|
||||
if (value instanceof String) {
|
||||
qtResult = powerLevels.getLevel((String) value);
|
||||
} else if (value instanceof Integer) {
|
||||
qtResult = powerLevels.getLevel((int) value);
|
||||
}
|
||||
|
||||
Assertions.assertNotNull(qtResult, "getLevel result is null: " + s);
|
||||
|
||||
result = qtResult.floatValue();
|
||||
float expected = (float) iter.next();
|
||||
Assertions.assertEquals(expected, result, .0001, "getLevel result incorrect");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
void test() {
|
||||
// Valid input
|
||||
testParse("5", 1, 0, "5 W", List.of("ON", 5f, 100, 5f, "OFF", 0f, 0, 0f, 50, 2.5f));
|
||||
testParse("2W", 1, 0, "2 W", List.of("ON", 2f, 100, 2f, "OFF", 0f, 0, 0f, 50, 1f));
|
||||
testParse(".5,2W", 2, 0, "0.5 W,2 W", List.of("ON", 2f, "OFF", .5f));
|
||||
testParse(" 0 , 1.5mW , 3W , 2.5mW, 5W ", 5, 0, "0 W,1.5 mW,2.5 mW,3 W,5 W",
|
||||
List.of(0, 0f, 100, 5f, 25, .0015f, 50, .0025f));
|
||||
testParse("OFF=0, LOW=2W, HIGH=5W,3W", 1, 3, "3 W,OFF=0 W,LOW=2 W,HIGH=5 W",
|
||||
List.of("OFF", 0f, "LOW", 2f, "HIGH", 5f));
|
||||
|
||||
// Invalid input
|
||||
assertThrows(Exception.class, () -> powerLevels.parse("Hello"));
|
||||
assertThrows(Exception.class, () -> powerLevels.parse("ON="));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 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.senseenergy.utils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* {@link TpLinkEncryptionTest }
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class TpLinkEncryptionTest {
|
||||
|
||||
@Test
|
||||
void test() throws UnsupportedEncodingException {
|
||||
String input = new String("Hello, this is a great day!");
|
||||
|
||||
byte[] encrypted = TpLinkEncryption.encrypt(input.getBytes("UTF-8"), 0);
|
||||
|
||||
byte[] unencrypted = TpLinkEncryption.decrypt(encrypted, 0);
|
||||
|
||||
assertEquals(input, new String(unencrypted, "UTF-8"));
|
||||
}
|
||||
}
|
|
@ -371,6 +371,7 @@
|
|||
<module>org.openhab.binding.senechome</module>
|
||||
<module>org.openhab.binding.seneye</module>
|
||||
<module>org.openhab.binding.sensebox</module>
|
||||
<module>org.openhab.binding.senseenergy</module>
|
||||
<module>org.openhab.binding.sensibo</module>
|
||||
<module>org.openhab.binding.sensorcommunity</module>
|
||||
<module>org.openhab.binding.serial</module>
|
||||
|
|
Loading…
Reference in New Issue