[juicenet] Initial contribution (#10768)

Signed-off-by: Jeff James <jeff@james-online.com>
This commit is contained in:
jsjames 2022-11-13 03:27:43 -08:00 committed by GitHub
parent fbd06ec709
commit 71d1226505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2117 additions and 0 deletions

View File

@ -153,6 +153,7 @@
/bundles/org.openhab.binding.jablotron/ @octa22
/bundles/org.openhab.binding.jeelink/ @vbier
/bundles/org.openhab.binding.jellyfin/ @GiviMAD
/bundles/org.openhab.binding.juicenet/ @jsjames
/bundles/org.openhab.binding.kaleidescape/ @mlobstein
/bundles/org.openhab.binding.keba/ @kgoderis
/bundles/org.openhab.binding.km200/ @Markinus

View File

@ -761,6 +761,11 @@
<artifactId>org.openhab.binding.jellyfin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.juicenet</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.kaleidescape</artifactId>

View File

@ -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

View File

@ -0,0 +1,270 @@
# JuiceNet Binding
The JuiceNet binding will interface with the cloud portal to get status and manage your JuiceBox EV charger(s).
In addition to getting the status of various items from the EV charger, it is also possible to start and stop charging sessions.
## Supported Things
This binding supports the following things:
| thing | type | description |
|---------- |-------- |------------------------------ |
| JuiceNet Account | Bridge | This represents the cloud account to interface with the JuiceNet API. |
| JuiceBox EV Charger | Device | This interfaces to a specific JuiceBox EV charger associated with the JuiceNet account. |
This binding should work with multiple JuiceBox EV chargers associated with the account, however it is currently only tested with a single EV charger.
### Discovery
Once a JuiceNet Account bridge has been created, any JuiceBox EV Chargers associated with this account will be discovered.
### Thing Configuration
The configuration required is to create a JuiceNet account thing and fill in the appropriate API token.
The API token can be found on the Account page at https://home.juice.net/Manage.
A JuiceBox EV Charger requires a a unitID which can also be found in the device settings at the JuiceNet web page.
## Channels
| channel | type | read-only | description |
|---------- |-------- |--------- | ------- |
| name | String | Y | Name of device.|
| chargingState | String | N | Current charging state (Start Charging, Smart Charging, Stop Charging). |
| state | String | Y | This is the current device state (Available, Plugged-In, Charging, Error, Disconnected). |
| message | String | Y | This is a message detailing the state of the EV charger. |
| override | Switch | Y | Smart charging is overridden. |
| chargingTimeLeft | Number:Time | Y | Charging time left (seconds). |
| plugUnplugTime | DateTime | Y | Last time of either plug-in or plug-out. |
| targetTime | DateTime | N | “Start charging” start time, or time to start when overriding smart charging. |
| unitTime | DateTime | Y | Current time on the unit. |
| temperature | Number:Temperature | Y | Current temperature at the unit. |
| currentLimit | Number:ElectricCurrent | N | Max charging current allowed. |
| current | Number:ElectricCurrent | Y | Current charging current. |
| voltage | Number:ElectricPotential | Y | Current voltage. |
| energy | Number:Energy | Y | Current amount of energy poured to the vehicle. |
| savings | Number | Y | Current session EV savings. |
| power | Number:Power | Y | Current charging power. |
| secondsCharging | Number:Time | Y | Charging time since plug-in time. |
| energyAtPlugin | Number:Energy | Y | Energy value at the plugging time. |
| energyToAdd | Number:Energy | N | Amount of energy to be added in current session. |
| lifetimeEnergy | Number:Energy | Y | Total energy delivered to vehicles during lifetime. |
| lifetimeSavings | Number | Y | EV driving saving during lifetime. |
| gasCost | Number | Y | Cost of gasoline used in savings calculations. |
| fuelConsumption | Number | Y | Miles per gallon used in savings calculations. |
| ecost | Number | Y | Cost of electricity from utility company. (currency/kWh) |
| energyPerMile | Number | Y | Energy per mile. |
| carDescription | String | Y | Car description of vehicle currently or last charged. |
| carBatterySize | Number:Energy | Y | Car battery pack size. |
| carBatteryRange | Number:Length | Y | Car range. |
| carChargingRate | Number:Power | Y | Car charging rate. |
## Full Example
### Things File
If configuring the binding with manual configuration an example thing file looks like this:
```
Bridge juicenet:account:myaccount [ apiToken="xxxx-xxxx-xxxx-xxxx-xxxxx" ] {
Thing device JamesCharger [ unitID="xxxxxxx" ]
}
```
### Items File
An example of an items file is here.
```
String JuiceNet_Name "Name" { channel="juicenet:device:myaccount:JamesCharger:name" }
String JuiceNet_State "Device State" { channel="juicenet:device:myaccount:JamesCharger:state" }
String JuiceNet_ChargingState "Charging State" { channel="juicenet:device:myaccount:JamesCharger:chargingState" }
String JuiceNet_Message "State Message" { channel="juicenet:device:myaccount:JamesCharger:message" }
Switch JuiceNet_Override "Override State" { channel="juicenet:device:myaccount:JamesCharger:override" }
DateTime JuiceNet_PlutUnplugTime "Plug/Unplug Time [%1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp]" { channel="juicenet:device:myaccount:JamesCharger:plugUnplugTime" }
DateTime JuiceNet_TargetTime "Target Time [%1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp]" { channel="juicenet:device:myaccount:JamesCharger:targetTime" }
Number:Time JuiceNet_ChargingTimeLeft "Charging Time Left [%.0f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:chargingTimeLeft" }
DateTime JuiceNet_UnitTime "Unit Time [%1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp]" { channel="juicenet:device:myaccount:JamesCharger:unitTime" }
Number:Temperature JuiceNet_Temperature "Temperature [%.0f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:temperature" }
Number:ElectricCurrent JuiceNet_CurrentLimit "Current Limit [%d %unit%]" { channel="juicenet:device:myaccount:JamesCharger:currentLimit" }
Number:ElectricCurrent JuiceNet_Current "Current [%.1f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:current" }
Number:ElectricPotential JuiceNet_Voltage "Voltage [%d %unit%]" { channel="juicenet:device:myaccount:JamesCharger:voltage" }
Number:Energy JuiceNet_Energy "Current Energy [%.1f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:energy" }
Number:Power JuiceNet_Power "Charging Power [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:power" }
Number JuiceNet_Savings "Savings [$%.2f]" { channel="juicenet:device:myaccount:JamesCharger:savings" }
Number:Time JuiceNet_ChargingTime "Charging Time [%.0f %unit%]" { channel="jjuicenet:device:myaccount:JamesCharger:chargingTime" }
Number:Energy JuiceNet_EnergyToAdd "Energy to Add [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:energyToAdd" }
Number:Energy JuiceNet_EnergyAtPlugin "Energy at Plugin [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:energyAtPlugin" }
Number:Energy JuiceNet_LifetimeEnergy "Lifetime Energy [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:lifetimeEnergy" }
Number JuiceNet_GasCost "Gas Cost [$%.2f]" { channel="juicenet:device:myaccount:JamesCharger:gasCost" }
Number JuiceNet_FuelConsumption "Fuel consumption [%.1f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:fuelConsumption" }
Number JuiceNet_Ecost "Utility Energy Cost [$%.2f]" { channel="juicenet:device:myaccount:JamesCharger:ecost" }
Number JuiceNet_LifetimeSavings "Lifetime Savings [$%.2f]" { channel="juicenet:device:myaccount:JamesCharger:lifetimeSavings" }
Number:Power JuiceNet_EnergyPerMile "Energy Hours Per Mile [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:energyPerMile" }
String JuiceNet_CarDescription "Car Description" { channel="juicenet:device:myaccount:JamesCharger:carDescription" }
Number:Length JuiceNet_CarBatteryRange "Mileage Range [%d %unit%]" { channel="juicenet:device:myaccount:JamesCharger:carBatteryRange" }
Number:Energy JuiceNet_CarBatterySize "Car Battery Pack Size [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:carBatterySize" }
Number:Power JuiceNet_CarChargineRage "Car Charging Rate [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:carChargingRate" }
```
## Widget
The following custom widget can be used with this binding.
![JuiceBox Widget](doc/widget.png)
```
uid: widget_JuiceBox
tags: []
props:
parameters:
- description: Prefix for the items with the data
label: Item prefix
name: prefix
required: false
type: TEXT
parameterGroups: []
timestamp: May 10, 2021, 2:38:55 PM
component: f7-card
config:
title: =items[props.prefix + "_Name"].state
style:
border-radius: var(--f7-card-expandable-border-radius)
--f7-card-header-border-color: none
slots:
default:
- component: f7-card-content
slots:
default:
- component: f7-row
config:
class:
- display-flex
- align-content-stretch
- align-items-center
slots:
default:
- component: f7-gauge
config:
type: semicircle
size: 270
value: =Number.parseFloat(items[props.prefix + "_CurrentEnergy"].state) / Number.parseFloat(items[props.prefix + "_CarBatteryPackSize"].state)
bg-color: transparent
border-bg-color: '=(items[props.prefix + "_DeviceState"].state === "charging") ? "#577543" : (items[props.prefix + "_DeviceState"].state === "plugged") ? "#8f6c2f" : "#595959"'
border-color: '=(items[props.prefix + "_DeviceState"].state === "charging") ? "#90d164" : (items[props.prefix + "_DeviceState"].state === "plugged") ? "#ed9c11" : "#adadad"'
borderWidth: 40
value-text: =items[props.prefix + "_CurrentEnergy"].displayState
value-text-color: '=(items[props.prefix + "_DeviceState"].state === "charging") ? "#90d164" : (items[props.prefix + "_DeviceState"].state === "plugged") ? "#ed9c11" : "#adadad"'
value-font-size: 20
value-font-weight: 500
label-text: =items[props.prefix + "_DeviceState"].displayState
label-text-color: white
label-font-size: 18
label-font-weight: 400
noBorder: true
outline: true
- component: f7-row
config:
class:
- display-flex
- justify-content-center
- align-content-stretch
- align-items-center
- margin-left
slots:
default:
- component: f7-segmented
config:
strong: true
style:
width: 80%
slots:
default:
- component: oh-button
config:
text: Start
color: blue
size: 24
active: =(items[props.prefix + "_ChargingState"].state === "start")
action: command
actionItem: =props.prefix + "_ChargingState"
actionCommand: start
- component: oh-button
config:
text: Smart
color: blue
size: 24
active: =(items[props.prefix + "_ChargingState"].state === 'smart')
action: command
actionItem: =props.prefix + "_ChargingState"
actionCommand: smart
- component: oh-button
config:
text: Stop
color: blue
size: 24
active: =(items[props.prefix + "_ChargingState"].state === "stop")
action: command
actionItem: =props.prefix + "_ChargingState"
actionCommand: stop
- component: f7-row
config:
class:
- display-flex
- justify-content-space-evenly
- align-content-stretch
- align-items-center
- height: 40px
style:
--f7-chip-font-size: 14px
--f7-chip-height: 28px
padding-top: 12px
slots:
default:
- component: f7-chip
config:
visible: =(items[props.prefix + "_DeviceState"].state === "charging")
text: '="Power: " + items[props.prefix + "_ChargingPower"].state'
iconF7: bolt_fill
media-bg-color: blue
bg-color: gray
label: hello
style:
padding-rightc: 12px
- component: f7-chip
config:
visible: =(items[props.prefix + "_DeviceState"].state === "charging")
text: '="Current: " + items[props.prefix + "_Current"].state'
iconF7: arrow_up_circl
media-bg-color: blue
bg-color: gray
- component: f7-chip
config:
text: '="Voltage: " + items[props.prefix + "_Voltage"].state'
iconF7: plusminus
media-bg-color: blue
bg-color: gray
- component: f7-chip
config:
visible: =(items[props.prefix + "_ChargingState"].state === 'smart')
text: '="Charge at: " + items[props.prefix + "_TargetTime"].displayState'
iconF7: clock
media-bg-color: blue
bg-color: gray
- component: f7-chip
config:
visible: =(items[props.prefix + "_DeviceState"].state === 'charging')
text: '="Charge Time Left: " + items[props.prefix + "_ChargingTimeLeft"].displayState'
iconF7: timer
media-bg-color: blue
bg-color: gray
- component: f7-card-footer
slots:
default:
- component: Label
config:
text: =items[props.prefix + "_CarDescription"].state
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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>3.4.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.juicenet</artifactId>
<name>openHAB Add-ons :: Bundles :: JuiceNet Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.juicenet-${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-juicenet" description="JuiceNet Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.juicenet/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link JuiceNetBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetBindingConstants {
private static final String BINDING_ID = "juicenet";
// List of Bridge Type
public static final String BRIDGE = "account";
// List of all Device Types
public static final String DEVICE = "device";
// List of all Bridge Thing Type UIDs
public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, BRIDGE);
// List of all Thing Type UIDs
public static final ThingTypeUID DEVICE_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE);
// Device config parameter
public static final String PARAMETER_UNIT_ID = "unitID";
// Device properties
public static final String PROPERTY_NAME = "name";
// List of all Channel ids
public static final String CHANNEL_NAME = "name";
public static final String CHANNEL_CHARGING_STATE = "chargingState";
public static final String CHANNEL_STATE = "state";
public static final String CHANNEL_MESSAGE = "message";
public static final String CHANNEL_OVERRIDE = "override";
public static final String CHANNEL_CHARGING_TIME_LEFT = "chargingTimeLeft";
public static final String CHANNEL_PLUG_UNPLUG_TIME = "plugUnplugTime";
public static final String CHANNEL_TARGET_TIME = "targetTime";
public static final String CHANNEL_UNIT_TIME = "unitTime";
public static final String CHANNEL_TEMPERATURE = "temperature";
public static final String CHANNEL_CURRENT_LIMIT = "currentLimit";
public static final String CHANNEL_CURRENT = "current";
public static final String CHANNEL_VOLTAGE = "voltage";
public static final String CHANNEL_ENERGY = "energy";
public static final String CHANNEL_SAVINGS = "savings";
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_CHARGING_TIME = "chargingTime";
public static final String CHANNEL_ENERGY_AT_PLUGIN = "energyAtPlugin";
public static final String CHANNEL_ENERGY_TO_ADD = "energyToAdd";
public static final String CHANNEL_LIFETIME_ENERGY = "lifetimeEnergy";
public static final String CHANNEL_LIFETIME_SAVINGS = "lifetimeSavings";
public static final String CHANNEL_GAS_COST = "gasCost";
public static final String CHANNEL_FUEL_CONSUMPTION = "fuelConsumption";
public static final String CHANNEL_ECOST = "ecost";
public static final String CHANNEL_ENERGY_PER_MILE = "energyPerMile";
public static final String CHANNEL_CAR_DESCRIPTION = "carDescription";
public static final String CHANNEL_CAR_BATTERY_SIZE = "carBatterySize";
public static final String CHANNEL_CAR_BATTERY_RANGE = "carBatteryRange";
public static final String CHANNEL_CAR_CHARGING_RATE = "carChargingRate";
}

View File

@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal;
import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.juicenet.internal.handler.JuiceNetBridgeHandler;
import org.openhab.binding.juicenet.internal.handler.JuiceNetDeviceHandler;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
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.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link JuiceNetHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.juicenet", service = ThingHandlerFactory.class)
public class JuiceNetHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(BRIDGE_THING_TYPE, DEVICE_THING_TYPE);
private final HttpClientFactory httpClientFactory;
private final TimeZoneProvider timeZoneProvider;
@Activate
public JuiceNetHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference TimeZoneProvider timeZoneProvider) {
this.httpClientFactory = httpClientFactory;
this.timeZoneProvider = timeZoneProvider;
}
@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 (thingTypeUID.equals(BRIDGE_THING_TYPE)) {
return new JuiceNetBridgeHandler((Bridge) thing, httpClientFactory.getCommonHttpClient());
} else if (thingTypeUID.equals(DEVICE_THING_TYPE)) {
return new JuiceNetDeviceHandler(thing, timeZoneProvider);
}
return null;
}
}

View File

@ -0,0 +1,228 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.api;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
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.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiDevice;
import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiDeviceStatus;
import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiInfo;
import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiTouSchedule;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
/**
* The {@link JuiceNetApi} is responsible for implementing the api interface to the JuiceNet cloud server
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApi {
private final Logger logger = LoggerFactory.getLogger(JuiceNetApi.class);
private static final String API_HOST = "https://jbv1-api.emotorwerks.com/";
private static final String API_ACCOUNT = API_HOST + "box_pin";
private static final String API_DEVICE = API_HOST + "box_api_secure";
private String apiToken = "";
private HttpClient httpClient;
private ThingUID bridgeUID;
public enum ApiCommand {
GET_ACCOUNT_UNITS("get_account_units", API_ACCOUNT),
GET_STATE("get_state", API_DEVICE),
SET_CHARGING_LIMIT("set_limit", API_DEVICE),
GET_SCHEDULE("get_schedule", API_DEVICE),
SET_SCHEDULE("set_schedule", API_DEVICE),
GET_INFO("get_info", API_DEVICE),
SET_OVERRIDE("set_override", API_DEVICE);
final String command;
final String uri;
ApiCommand(String command, String uri) {
this.command = command;
this.uri = uri;
}
}
public JuiceNetApi(HttpClient httpClient, ThingUID bridgeUID) {
this.bridgeUID = bridgeUID;
this.httpClient = httpClient;
}
public void setApiToken(String apiToken) {
this.apiToken = apiToken;
}
public List<JuiceNetApiDevice> queryDeviceList() throws JuiceNetApiException, InterruptedException {
JuiceNetApiDevice[] listDevices;
try {
JsonObject jsonResponse = postApiCommand(ApiCommand.GET_ACCOUNT_UNITS, null);
JsonElement unitsElement = jsonResponse.get("units");
if (unitsElement == null) {
throw new JuiceNetApiException("getDevices from Juicenet API failed, no 'units' element in response.");
}
listDevices = new Gson().fromJson(unitsElement.getAsJsonArray(), JuiceNetApiDevice[].class);
} catch (JsonSyntaxException e) {
throw new JuiceNetApiException("getDevices from JuiceNet API failed, invalid JSON list.");
} catch (IllegalStateException e) {
throw new JuiceNetApiException("getDevices from JuiceNet API failed - did not return valid array.");
}
return Arrays.asList(listDevices);
}
public JuiceNetApiDeviceStatus queryDeviceStatus(String token) throws JuiceNetApiException, InterruptedException {
JuiceNetApiDeviceStatus deviceStatus;
try {
JsonObject jsonResponse = postApiCommand(ApiCommand.GET_STATE, token);
deviceStatus = new Gson().fromJson(jsonResponse, JuiceNetApiDeviceStatus.class);
} catch (JsonSyntaxException e) {
throw new JuiceNetApiException("queryDeviceStatus from JuiceNet API failed, invalid JSON list.");
} catch (IllegalStateException e) {
throw new JuiceNetApiException("queryDeviceStatus from JuiceNet API failed - did not return valid array.");
}
return Objects.requireNonNull(deviceStatus);
}
public JuiceNetApiInfo queryInfo(String token) throws InterruptedException, JuiceNetApiException {
JuiceNetApiInfo info;
try {
JsonObject jsonResponse = postApiCommand(ApiCommand.GET_INFO, token);
info = new Gson().fromJson(jsonResponse, JuiceNetApiInfo.class);
} catch (JsonSyntaxException e) {
throw new JuiceNetApiException("queryInfo from JuiceNet API failed, invalid JSON list.");
} catch (IllegalStateException e) {
throw new JuiceNetApiException("queryInfo from JuiceNet API failed - did not return valid array.");
}
return Objects.requireNonNull(info);
}
public JuiceNetApiTouSchedule queryTOUSchedule(String token) throws InterruptedException, JuiceNetApiException {
JuiceNetApiTouSchedule deviceTouSchedule;
try {
JsonObject jsonResponse = postApiCommand(ApiCommand.GET_SCHEDULE, token);
deviceTouSchedule = new Gson().fromJson(jsonResponse, JuiceNetApiTouSchedule.class);
} catch (JsonSyntaxException e) {
throw new JuiceNetApiException("queryTOUSchedule from JuiceNet API failed, invalid JSON list.");
} catch (IllegalStateException e) {
throw new JuiceNetApiException("queryTOUSchedule from JuiceNet API failed - did not return valid array.");
}
return Objects.requireNonNull(deviceTouSchedule);
}
public void setOverride(String token, int energy_at_plugin, Long override_time, int energy_to_add)
throws InterruptedException, JuiceNetApiException {
Map<String, Object> params = new HashMap<>();
params.put("energy_at_plugin", Integer.toString(energy_at_plugin));
params.put("override_time", Long.toString(energy_at_plugin));
params.put("energy_to_add", Integer.toString(energy_at_plugin));
postApiCommand(ApiCommand.SET_OVERRIDE, token, params);
}
public void setCurrentLimit(String token, int limit) throws InterruptedException, JuiceNetApiException {
Map<String, Object> params = new HashMap<>();
params.put("amperage", Integer.toString(limit));
postApiCommand(ApiCommand.SET_OVERRIDE, token, params);
}
public JsonObject postApiCommand(ApiCommand cmd, @Nullable String token)
throws InterruptedException, JuiceNetApiException {
Map<String, Object> params = new HashMap<>();
return postApiCommand(cmd, token, params);
}
public JsonObject postApiCommand(ApiCommand cmd, @Nullable String token, Map<String, Object> params)
throws InterruptedException, JuiceNetApiException {
Request request = httpClient.POST(cmd.uri);
request.header(HttpHeader.CONTENT_TYPE, "application/json");
// Add required params
params.put("cmd", cmd.command);
params.put("device_id", bridgeUID.getAsString());
params.put("account_token", apiToken);
if (token != null) {
params.put("token", token);
}
JsonObject jsonResponse;
try {
request.content(new StringContentProvider(new Gson().toJson(params)), "application/json");
ContentResponse response = request.send();
if (response.getStatus() != HttpStatus.OK_200) {
throw new JuiceNetApiException(
cmd.command + "from JuiceNet API unsucessful, please check configuation. (HTTP code :"
+ response.getStatus() + ").");
}
String responseString = response.getContentAsString();
logger.trace("{}", responseString);
jsonResponse = JsonParser.parseString(responseString).getAsJsonObject();
JsonElement successElement = jsonResponse.get("success");
if (successElement == null) {
throw new JuiceNetApiException(
cmd.command + " from JuiceNet API failed, 'success' element missing from response.");
}
boolean success = successElement.getAsBoolean();
if (!success) {
throw new JuiceNetApiException(cmd.command + " from JuiceNet API failed, please check configuration.");
}
} catch (IllegalStateException e) {
throw new JuiceNetApiException(cmd.command + " from JuiceNet API failed, invalid JSON.");
} catch (TimeoutException e) {
throw new JuiceNetApiException(cmd.command + " from JuiceNet API timeout.");
} catch (ExecutionException e) {
throw new JuiceNetApiException(cmd.command + " from JuiceNet API execution issue.");
}
return jsonResponse;
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link JuiceNetApiException} implements an API Exception
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiException extends Exception {
private static final long serialVersionUID = 5421236828224242152L;
public JuiceNetApiException(String message) {
super(message);
}
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiCar } implements DTO for Car API call
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiCar {
@SerializedName("car_id")
public int carId;
public String description = "";
@SerializedName("battery_size_wh")
public int batterySizeWH;
@SerializedName("battery_range_m")
public int batteryRangeM;
@SerializedName("charging_rate_w")
public int chargingRateW;
@SerializedName("model_id")
public String modelId = "";
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiDevice } implements DTO for Device Info API call
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiDevice {
public String name = "";
public String token = "";
@SerializedName("unit_id")
public String unitId = "";
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetDeviceChargingStatus } implements DTO for device charging status
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiDeviceChargingStatus {
@SerializedName("amps_limit")
public int ampsLimit;
@SerializedName("amps_current")
public float ampsCurrent;
public int voltage;
@SerializedName("wh_energy")
public int whEnergy;
public int savings;
@SerializedName("watt_power")
public int wattPower;
@SerializedName("seconds_charging")
public int secondsCharging;
@SerializedName("wh_energy_at_plugin")
public int whEnergyAtPlugin;
@SerializedName("wh_energy_to_add")
public int whEnergyToAdd;
public int flags;
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiDeviceLifetimeStatus } implements DTO for Device Lifetime Status
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiDeviceLifetimeStatus {
@SerializedName("wh_energy")
public int whEnergy;
public int savings;
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiDeviceStatus } implements DTO for Device Status
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiDeviceStatus {
@SerializedName("ID")
public String id = "";
@SerializedName("info_timestamp")
public Long infoTimestamp = (long) 0;
@SerializedName("show_override")
public boolean showOverride;
public String state = "";
public JuiceNetApiDeviceChargingStatus charging = new JuiceNetApiDeviceChargingStatus();
public JuiceNetApiDeviceLifetimeStatus lifetime = new JuiceNetApiDeviceLifetimeStatus();
@SerializedName("charging_time_left")
public int chargingTimeLeft;
@SerializedName("plug_unplug_time")
public Long plugUnplugTime = (long) 0;
@SerializedName("target_time")
public Long targetTime = (long) 0;
@SerializedName("unit_time")
public Long unitTime = (long) 0;
@SerializedName("utc_time")
public Long utcTime = (long) 0;
@SerializedName("default_target_time")
public long defaultTargetTime = 0;
@SerializedName("car_id")
public int carId;
public int temperature;
public String message = "";
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiInfo } implements DTO for Info
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiInfo {
public String name = "";
public String address = "";
public String city = "";
public String zip = "";
@SerializedName("country_code")
public String countryCode = "";
public String ip = "";
@SerializedName("gascost")
public int gasCost;
public int mpg;
public int ecost;
@SerializedName("whpermile")
public int whPerMile;
public String timeZoneId = "";
@SerializedName("amps_wire_rating")
public int ampsWireRating;
@SerializedName("amps_unit_rating")
public int ampsUnitRating;
public JuiceNetApiCar[] cars = {};
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiTouDay } implements DTO for TOU settings
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiTouDay {
public int start;
public int end;
@SerializedName("car_ready_by")
public int carReadyBy;
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link JuiceNetApiTouSchedule } implements DTO for TOU schedule
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiTouSchedule {
public String type = "";
public JuiceNetApiTouDay weekday = new JuiceNetApiTouDay();
public JuiceNetApiTouDay weenend = new JuiceNetApiTouDay();
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link JuiceNetBridgeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetBridgeConfiguration {
public String apiToken = "";
public int refreshInterval = 60;
}

View File

@ -0,0 +1,97 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.discovery;
import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.juicenet.internal.handler.JuiceNetBridgeHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link JuiceNetDiscoveryService} discovers all devices/zones reported by the FlumeTech Cloud. This requires the
* api
* key to get access to the cloud data.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetDiscoveryService extends AbstractDiscoveryService
implements DiscoveryService, ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(JuiceNetDiscoveryService.class);
private @Nullable JuiceNetBridgeHandler bridgeHandler;
private static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Set.of(DEVICE_THING_TYPE);
public JuiceNetDiscoveryService() {
super(DISCOVERABLE_THING_TYPES_UIDS, 0, false);
}
@Override
public void activate() {
super.activate(null);
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
protected synchronized void startScan() {
Objects.requireNonNull(bridgeHandler).iterateApiDevices();
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof JuiceNetBridgeHandler) {
JuiceNetBridgeHandler bridgeHandler = (JuiceNetBridgeHandler) handler;
bridgeHandler.setDiscoveryService(this);
this.bridgeHandler = bridgeHandler;
} else {
this.bridgeHandler = null;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.bridgeHandler;
}
public void notifyDiscoveryDevice(String id, String name) {
JuiceNetBridgeHandler bridgeHandler = this.bridgeHandler;
Objects.requireNonNull(bridgeHandler, "Discovery with null bridgehandler.");
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
ThingUID uid = new ThingUID(DEVICE_THING_TYPE, bridgeUID, id);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
.withProperty(PARAMETER_UNIT_ID, id).withLabel(name).build();
thingDiscovered(result);
logger.debug("Discovered JuiceNetDevice {} - {}", uid, name);
}
}

View File

@ -0,0 +1,201 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.handler;
import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.juicenet.internal.api.JuiceNetApi;
import org.openhab.binding.juicenet.internal.api.JuiceNetApiException;
import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiDevice;
import org.openhab.binding.juicenet.internal.config.JuiceNetBridgeConfiguration;
import org.openhab.binding.juicenet.internal.discovery.JuiceNetDiscoveryService;
import org.openhab.core.config.core.Configuration;
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;
/**
* The {@link JuiceNetBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(JuiceNetBridgeHandler.class);
private JuiceNetBridgeConfiguration config = new JuiceNetBridgeConfiguration();
private final JuiceNetApi api;
public JuiceNetApi getApi() {
return api;
}
private @Nullable ScheduledFuture<?> pollingJob;
private @Nullable JuiceNetDiscoveryService discoveryService;
public JuiceNetBridgeHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.api = new JuiceNetApi(httpClient, getThing().getUID());
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void initialize() {
config = getConfigAs(JuiceNetBridgeConfiguration.class);
logger.trace("Bridge initialized: {}", Objects.requireNonNull(getThing()).getUID());
api.setApiToken(config.apiToken);
updateStatus(ThingStatus.UNKNOWN);
// Bridge will go online after the first successful API call in iterateApiDevices. iterateApiDevices will be
// called when a child device attempts to goOnline and needs to retrieve the api token
pollingJob = scheduler.scheduleWithFixedDelay(this::pollDevices, 10, config.refreshInterval, TimeUnit.SECONDS);
// Call here in order to discover any devices.
iterateApiDevices();
}
@Override
public void dispose() {
logger.debug("Handler disposed.");
ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null) {
pollingJob.cancel(true);
this.pollingJob = null;
}
}
public void setDiscoveryService(JuiceNetDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
// Call here to set the Api Token for any newly initialized Child devices
iterateApiDevices();
}
/**
* Get the services registered for this bridge. Provides the discovery service.
*/
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(JuiceNetDiscoveryService.class);
}
public void handleApiException(Exception e) {
if (e instanceof JuiceNetApiException) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.toString());
} else if (e instanceof InterruptedException) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.toString());
Thread.currentThread().interrupt();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.toString());
}
}
@Nullable
public Thing getThingById(String id) {
List<Thing> childThings = getThing().getThings();
for (Thing childThing : childThings) {
Configuration configuration = childThing.getConfiguration();
String childId = configuration.get(PARAMETER_UNIT_ID).toString();
if (childId.equals(id)) {
return childThing;
}
}
return null;
}
// This function will query the list of devices from the API and then set the name/token in the child handlers. If a
// child does not exist, it will notify the Discovery service. If it is successful, it will ensure the bridge status
// is updated
// to ONLINE.
public void iterateApiDevices() {
List<JuiceNetApiDevice> listDevices;
try {
listDevices = api.queryDeviceList();
} catch (JuiceNetApiException | InterruptedException e) {
handleApiException(e);
return;
}
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
JuiceNetDiscoveryService discoveryService = this.discoveryService;
for (JuiceNetApiDevice dev : listDevices) {
Thing childThing = getThingById(dev.unitId);
if (childThing == null) {
if (discoveryService != null) {
discoveryService.notifyDiscoveryDevice(dev.unitId, dev.name);
}
} else {
JuiceNetDeviceHandler childHandler = (JuiceNetDeviceHandler) childThing.getHandler();
if (childHandler != null) {
childHandler.setNameAndToken(dev.name, dev.token);
}
}
}
}
private void pollDevices() {
List<Thing> things = getThing().getThings();
for (Thing t : things) {
if (!t.getThingTypeUID().equals(DEVICE_THING_TYPE)) {
continue;
}
JuiceNetDeviceHandler handler = (JuiceNetDeviceHandler) t.getHandler();
if (handler == null) {
logger.trace("no handler for thing: {}", t.getUID());
continue;
}
handler.queryDeviceStatusAndInfo();
}
}
}

View File

@ -0,0 +1,360 @@
/**
* Copyright (c) 2010-2022 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.juicenet.internal.handler;
import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.juicenet.internal.api.JuiceNetApi;
import org.openhab.binding.juicenet.internal.api.JuiceNetApiException;
import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiCar;
import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiDeviceStatus;
import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiInfo;
import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiTouSchedule;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
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.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link JuiceNetDeviceHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetDeviceHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(JuiceNetDeviceHandler.class);
private final TimeZoneProvider timeZoneProvider;
// properties
private String name = "";
private String token = "";
private long targetTimeTou = 0;
private long lastInfoTimestamp = 0;
JuiceNetApiDeviceStatus deviceStatus = new JuiceNetApiDeviceStatus();
JuiceNetApiInfo deviceInfo = new JuiceNetApiInfo();
JuiceNetApiTouSchedule deviceTouSchedule = new JuiceNetApiTouSchedule();
JuiceNetApiCar deviceCar = new JuiceNetApiCar();
public JuiceNetDeviceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
super(thing);
this.timeZoneProvider = timeZoneProvider;
}
public void setNameAndToken(String name, String token) {
logger.trace("setNameAndToken");
this.token = token;
if (!name.equals(this.name)) {
updateProperty(PROPERTY_NAME, name);
this.name = name;
}
if (getThing().getStatus() != ThingStatus.ONLINE) {
goOnline();
}
}
@Override
public void initialize() {
logger.trace("Device initialized: {}", Objects.requireNonNull(getThing().getUID()));
Configuration configuration = getThing().getConfiguration();
String stringId = configuration.get(PARAMETER_UNIT_ID).toString();
if (stringId.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.configuration-error.id-missing");
return;
}
updateStatus(ThingStatus.UNKNOWN);
// This device will go ONLINE on the first successful API call in queryDeviceStatusAndInfo
}
private void handleApiException(Exception e) {
if (e instanceof JuiceNetApiException) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.toString());
} else if (e instanceof InterruptedException) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.toString());
Thread.currentThread().interrupt();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.toString());
}
}
private void goOnline() {
logger.trace("goOnline");
if (this.getThing().getStatus() == ThingStatus.ONLINE) {
return;
}
if (token.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.configuration-error.non-existent-device");
return;
}
try {
tryQueryDeviceStatusAndInfo();
} catch (JuiceNetApiException | InterruptedException e) {
handleApiException(e);
return;
}
updateStatus(ThingStatus.ONLINE);
}
@Nullable
private JuiceNetApi getApi() {
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.configuration-error.bridge-missing");
return null;
}
BridgeHandler handler = Objects.requireNonNull(bridge.getHandler());
return ((JuiceNetBridgeHandler) handler).getApi();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
JuiceNetApi api = getApi();
if (api == null) {
return;
}
if (command instanceof RefreshType) {
switch (channelUID.getId()) {
case CHANNEL_NAME:
case CHANNEL_STATE:
case CHANNEL_MESSAGE:
case CHANNEL_OVERRIDE:
case CHANNEL_CHARGING_TIME_LEFT:
case CHANNEL_PLUG_UNPLUG_TIME:
case CHANNEL_TARGET_TIME:
case CHANNEL_UNIT_TIME:
case CHANNEL_TEMPERATURE:
case CHANNEL_CURRENT_LIMIT:
case CHANNEL_CURRENT:
case CHANNEL_VOLTAGE:
case CHANNEL_ENERGY:
case CHANNEL_SAVINGS:
case CHANNEL_POWER:
case CHANNEL_CHARGING_TIME:
case CHANNEL_ENERGY_AT_PLUGIN:
case CHANNEL_ENERGY_TO_ADD:
case CHANNEL_LIFETIME_ENERGY:
case CHANNEL_LIFETIME_SAVINGS:
case CHANNEL_CAR_DESCRIPTION:
case CHANNEL_CAR_BATTERY_SIZE:
case CHANNEL_CAR_BATTERY_RANGE:
case CHANNEL_CAR_CHARGING_RATE:
refreshStatusChannels();
break;
case CHANNEL_GAS_COST:
case CHANNEL_FUEL_CONSUMPTION:
case CHANNEL_ECOST:
case CHANNEL_ENERGY_PER_MILE:
refreshInfoChannels();
break;
}
return;
}
try {
switch (channelUID.getId()) {
case CHANNEL_CURRENT_LIMIT:
int limit = ((QuantityType<?>) command).intValue();
api.setCurrentLimit(Objects.requireNonNull(token), limit);
break;
case CHANNEL_TARGET_TIME: {
int energyAtPlugin = 0;
int energyToAdd = deviceCar.batterySizeWH;
if (!(command instanceof DateTimeType)) {
logger.info("Target Time is not an instance of DateTimeType");
return;
}
ZonedDateTime datetime = ((DateTimeType) command).getZonedDateTime();
Long targetTime = datetime.toEpochSecond() + datetime.get(ChronoField.OFFSET_SECONDS);
logger.debug("DateTime: {} - {}", datetime.toString(), targetTime);
api.setOverride(Objects.requireNonNull(token), energyAtPlugin, targetTime, energyToAdd);
break;
}
case CHANNEL_CHARGING_STATE: {
String state = ((StringType) command).toString();
Long overrideTime = deviceStatus.unitTime;
int energyAtPlugin = 0;
int energyToAdd = deviceCar.batterySizeWH;
switch (state) {
case "stop":
if (targetTimeTou == 0) {
targetTimeTou = deviceStatus.targetTime;
}
overrideTime = deviceStatus.unitTime + 31556926;
break;
case "start":
if (targetTimeTou == 0) {
targetTimeTou = deviceStatus.targetTime;
}
overrideTime = deviceStatus.unitTime;
break;
case "smart":
overrideTime = deviceStatus.defaultTargetTime;
break;
}
api.setOverride(Objects.requireNonNull(token), energyAtPlugin, overrideTime, energyToAdd);
break;
}
}
} catch (JuiceNetApiException | InterruptedException e) {
handleApiException(e);
return;
}
}
private void tryQueryDeviceStatusAndInfo() throws JuiceNetApiException, InterruptedException {
String apiToken = Objects.requireNonNull(this.token);
JuiceNetApi api = getApi();
if (api == null) {
return;
}
deviceStatus = api.queryDeviceStatus(apiToken);
if (deviceStatus.infoTimestamp > lastInfoTimestamp) {
lastInfoTimestamp = deviceStatus.infoTimestamp;
deviceInfo = api.queryInfo(apiToken);
deviceTouSchedule = api.queryTOUSchedule(apiToken);
refreshInfoChannels();
}
int carId = deviceStatus.carId;
for (JuiceNetApiCar car : deviceInfo.cars) {
if (car.carId == carId) {
this.deviceCar = car;
break;
}
}
refreshStatusChannels();
}
public void queryDeviceStatusAndInfo() {
logger.trace("queryStatusAndInfo");
ThingStatus status = getThing().getStatus();
if (status != ThingStatus.ONLINE) {
goOnline();
return;
}
try {
tryQueryDeviceStatusAndInfo();
} catch (JuiceNetApiException | InterruptedException e) {
handleApiException(e);
return;
}
}
private ZonedDateTime toZonedDateTime(long localEpochSeconds) {
return Instant.ofEpochSecond(localEpochSeconds).atZone(timeZoneProvider.getTimeZone());
}
private void refreshStatusChannels() {
updateState(CHANNEL_STATE, new StringType(deviceStatus.state));
if (deviceStatus.targetTime <= deviceStatus.unitTime) {
updateState(CHANNEL_CHARGING_STATE, new StringType("start"));
} else if ((deviceStatus.targetTime - deviceStatus.unitTime) < TimeUnit.DAYS.toSeconds(2)) {
updateState(CHANNEL_CHARGING_STATE, new StringType("smart"));
} else {
updateState(CHANNEL_CHARGING_STATE, new StringType("stop"));
}
updateState(CHANNEL_MESSAGE, new StringType(deviceStatus.message));
updateState(CHANNEL_OVERRIDE, OnOffType.from(deviceStatus.showOverride));
updateState(CHANNEL_CHARGING_TIME_LEFT, new QuantityType<>(deviceStatus.chargingTimeLeft, Units.SECOND));
updateState(CHANNEL_PLUG_UNPLUG_TIME, new DateTimeType(toZonedDateTime(deviceStatus.plugUnplugTime)));
updateState(CHANNEL_TARGET_TIME, new DateTimeType(toZonedDateTime(deviceStatus.targetTime)));
updateState(CHANNEL_UNIT_TIME, new DateTimeType(toZonedDateTime(deviceStatus.utcTime)));
updateState(CHANNEL_TEMPERATURE, new QuantityType<>(deviceStatus.temperature, SIUnits.CELSIUS));
updateState(CHANNEL_CURRENT_LIMIT, new QuantityType<>(deviceStatus.charging.ampsLimit, Units.AMPERE));
updateState(CHANNEL_CURRENT, new QuantityType<>(deviceStatus.charging.ampsCurrent, Units.AMPERE));
updateState(CHANNEL_VOLTAGE, new QuantityType<>(deviceStatus.charging.voltage, Units.VOLT));
updateState(CHANNEL_ENERGY, new QuantityType<>(deviceStatus.charging.whEnergy, Units.WATT_HOUR));
updateState(CHANNEL_SAVINGS, new DecimalType(deviceStatus.charging.savings / 100.0));
updateState(CHANNEL_POWER, new QuantityType<>(deviceStatus.charging.wattPower, Units.WATT));
updateState(CHANNEL_CHARGING_TIME, new QuantityType<>(deviceStatus.charging.secondsCharging, Units.SECOND));
updateState(CHANNEL_ENERGY_AT_PLUGIN,
new QuantityType<>(deviceStatus.charging.whEnergyAtPlugin, Units.WATT_HOUR));
updateState(CHANNEL_ENERGY_TO_ADD, new QuantityType<>(deviceStatus.charging.whEnergyToAdd, Units.WATT_HOUR));
updateState(CHANNEL_LIFETIME_ENERGY, new QuantityType<>(deviceStatus.lifetime.whEnergy, Units.WATT_HOUR));
updateState(CHANNEL_LIFETIME_SAVINGS, new DecimalType(deviceStatus.lifetime.savings / 100.0));
// update Car items
updateState(CHANNEL_CAR_DESCRIPTION, new StringType(deviceCar.description));
updateState(CHANNEL_CAR_BATTERY_SIZE, new QuantityType<>(deviceCar.batterySizeWH, Units.WATT_HOUR));
updateState(CHANNEL_CAR_BATTERY_RANGE, new QuantityType<>(deviceCar.batteryRangeM, ImperialUnits.MILE));
updateState(CHANNEL_CAR_CHARGING_RATE, new QuantityType<>(deviceCar.chargingRateW, Units.WATT));
}
private void refreshInfoChannels() {
updateState(CHANNEL_NAME, new StringType(name));
updateState(CHANNEL_GAS_COST, new DecimalType(deviceInfo.gasCost / 100.0));
// currently there is no unit defined for fuel consumption
updateState(CHANNEL_FUEL_CONSUMPTION, new DecimalType(deviceInfo.mpg));
updateState(CHANNEL_ECOST, new DecimalType(deviceInfo.ecost / 100.0));
updateState(CHANNEL_ENERGY_PER_MILE, new DecimalType(deviceInfo.whPerMile));
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="juicenet" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>JuiceNet Binding</name>
<description>This is the binding supporting the JuiceNet EV charger.</description>
</binding:binding>

View File

@ -0,0 +1,98 @@
# binding
binding.juicenet.name = JuiceNet Binding
binding.juicenet.description = This is the binding supporting the JuiceNet EV charger.
# thing types
thing-type.juicenet.account.label = JuiceNet Account
thing-type.juicenet.account.description = This is the account for which your device(s) are registered at home.juice.net.
thing-type.juicenet.device.label = JuiceBox Charger
thing-type.juicenet.device.description = JuiceBox EV Charger
# thing types config
thing-type.config.juicenet.account.apiToken.label = API Token
thing-type.config.juicenet.account.apiToken.description = API Token from the user profile page. (https://home.juice.net/Manage)
thing-type.config.juicenet.account.refreshInterval.label = Refresh Interval
thing-type.config.juicenet.account.refreshInterval.description = Interval the device is polled in seconds.
thing-type.config.juicenet.device.unitID.label = Unit ID
thing-type.config.juicenet.device.unitID.description = EV charger Unit ID from the JuiceNet webpage. (https://home.juice.net)
# channel types
channel-type.juicenet.carBatteryRange.label = Mileage Range
channel-type.juicenet.carBatteryRange.description = Car distance range.
channel-type.juicenet.carBatterySize.label = Car Battery Pack Size
channel-type.juicenet.carBatterySize.description = Car battery pack size.
channel-type.juicenet.carChargingRate.label = Car Charging Rate
channel-type.juicenet.carChargingRate.description = Car charging rate.
channel-type.juicenet.carDescription.label = Car Description
channel-type.juicenet.carDescription.description = Car description of vehicle currently or last charged.
channel-type.juicenet.chargingState.label = Charging State
channel-type.juicenet.chargingState.description = The charging state (Start Charging, Smart Charging, Stop Charging).
channel-type.juicenet.chargingState.state.option.start = Start Charging
channel-type.juicenet.chargingState.state.option.smart = Smart Charging
channel-type.juicenet.chargingState.state.option.stop = Stop Charging
channel-type.juicenet.chargingTime.label = Charging Time
channel-type.juicenet.chargingTime.description = Charging time since plug-in time.
channel-type.juicenet.chargingTimeLeft.label = Charging Time Left
channel-type.juicenet.chargingTimeLeft.description = Charging time left.
channel-type.juicenet.current.label = Current
channel-type.juicenet.current.description = Current charging current.
channel-type.juicenet.currentLimit.label = Current Limit
channel-type.juicenet.currentLimit.description = Max charging current allowed.
channel-type.juicenet.ecost.label = Utility Energy Cost
channel-type.juicenet.ecost.description = Cost of electricity from utility company. (currency / kWh)
channel-type.juicenet.energy.label = Current Energy
channel-type.juicenet.energy.description = Current power level of vehicle.
channel-type.juicenet.energyAtPlugin.label = Energy at Plugin
channel-type.juicenet.energyAtPlugin.description = Energy value at the plugging time.
channel-type.juicenet.energyPerMile.label = Energy Hours Per Mile
channel-type.juicenet.energyPerMile.description = Energy Hours Per Mile.
channel-type.juicenet.energyToAdd.label = Energy to Add
channel-type.juicenet.energyToAdd.description = Amount of energy to be added in current session.
channel-type.juicenet.fuelConsumption.label = Fuel consumption
channel-type.juicenet.fuelConsumption.description = Distance per volume (mpg) used in savings calculations.
channel-type.juicenet.gasCost.label = Gas Cost
channel-type.juicenet.gasCost.description = Cost of gasoline used in savings calculations.
channel-type.juicenet.lifetimeEnergy.label = Lifetime Energy
channel-type.juicenet.lifetimeEnergy.description = Total energy delivered to vehicles during lifetime.
channel-type.juicenet.lifetimeSavings.label = Lifetime Savings
channel-type.juicenet.lifetimeSavings.description = EV driving saving during lifetime.
channel-type.juicenet.message.label = State Message
channel-type.juicenet.message.description = This is a message detailing the state of the EV charger.
channel-type.juicenet.name.label = Name
channel-type.juicenet.name.description = Juice Box name.
channel-type.juicenet.override.label = Override State
channel-type.juicenet.override.description = Smart charging is overridden.
channel-type.juicenet.plugUnplugTime.label = Plug/Unplug Time
channel-type.juicenet.plugUnplugTime.description = Last time of either plug-in or plug-out.
channel-type.juicenet.plugUnplugTime.state.pattern = %1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp
channel-type.juicenet.power.label = Charging Power
channel-type.juicenet.power.description = Current charging power.
channel-type.juicenet.savings.label = Savings
channel-type.juicenet.savings.description = Current session EV savings.
channel-type.juicenet.state.label = Device State
channel-type.juicenet.state.description = This is the current device state (Available, Plugged-In, Charging, Error, Disconnected).
channel-type.juicenet.state.state.option.standby = Available
channel-type.juicenet.state.state.option.plugged = Plugged-In
channel-type.juicenet.state.state.option.charging = Charging
channel-type.juicenet.state.state.option.error = Error
channel-type.juicenet.state.state.option.disconnect = Disconnected
channel-type.juicenet.targetTime.label = Target Time
channel-type.juicenet.targetTime.description = “Start charging” start time, or time to start when overriding smart charging.
channel-type.juicenet.targetTime.state.pattern = %1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp
channel-type.juicenet.temperature.label = Temperature
channel-type.juicenet.temperature.description = Current temperature at the unit.
channel-type.juicenet.unitTime.label = Unit Time
channel-type.juicenet.unitTime.description = Current time on the unit.
channel-type.juicenet.unitTime.state.pattern = %1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp
channel-type.juicenet.voltage.label = Voltage
channel-type.juicenet.voltage.description = Current voltage.
# offline configuration errors
offline.configuration-error.id-missing = Must include an id in the configuration for the device.
offline.configuration-error.non-existent-device = Device does not exist as part of this JuiceNet Account
offline.configuration-error.bridge-missing = The JuiceBox device must be associated with a JuiceNet Account bridge

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="juicenet"
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="account">
<label>JuiceNet Account</label>
<description>This is the account for which your device(s) are registered at home.juice.net.</description>
<config-description>
<parameter name="apiToken" type="text" required="true">
<label>API Token</label>
<description>API Token from the user profile page. (https://home.juice.net/Manage) </description>
</parameter>
<parameter name="refreshInterval" type="integer" unit="s" min="60">
<label>Refresh Interval</label>
<description>Interval the device is polled in seconds.</description>
<default>60</default>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,288 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="juicenet"
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">
<thing-type id="device">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>JuiceBox Charger</label>
<description>JuiceBox EV Charger</description>
<channels>
<channel id="name" typeId="name"/>
<channel id="chargingState" typeId="chargingState"/>
<channel id="state" typeId="state"/>
<channel id="message" typeId="message"/>
<channel id="override" typeId="override"/>
<channel id="chargingTimeLeft" typeId="chargingTimeLeft"/>
<channel id="plugUnplugTime" typeId="plugUnplugTime"/>
<channel id="targetTime" typeId="targetTime"/>
<channel id="unitTime" typeId="unitTime"/>
<channel id="temperature" typeId="temperature"/>
<channel id="currentLimit" typeId="currentLimit"/>
<channel id="current" typeId="current"/>
<channel id="voltage" typeId="voltage"/>
<channel id="energy" typeId="energy"/>
<channel id="savings" typeId="savings"/>
<channel id="power" typeId="power"/>
<channel id="chargingTime" typeId="chargingTime"/>
<channel id="energyAtPlugin" typeId="energyAtPlugin"/>
<channel id="energyToAdd" typeId="energyToAdd"/>
<channel id="lifetimeEnergy" typeId="lifetimeEnergy"/>
<channel id="lifetimeSavings" typeId="lifetimeSavings"/>
<channel id="gasCost" typeId="gasCost"/>
<channel id="fuelConsumption" typeId="fuelConsumption"/>
<channel id="ecost" typeId="ecost"/>
<channel id="energyPerMile" typeId="energyPerMile"/>
<channel id="carDescription" typeId="carDescription"/>
<channel id="carBatterySize" typeId="carBatterySize"/>
<channel id="carBatteryRange" typeId="carBatteryRange"/>
<channel id="carChargingRate" typeId="carChargingRate"/>
</channels>
<properties>
<property name="name"></property>
</properties>
<representation-property>unitID</representation-property>
<config-description>
<parameter name="unitID" type="text" required="true">
<label>Unit ID</label>
<description>EV charger Unit ID from the JuiceNet webpage. (https://home.juice.net) </description>
</parameter>
</config-description>
</thing-type>
<channel-type id="name">
<item-type>String</item-type>
<label>Name</label>
<description>Juice Box name.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="chargingState">
<item-type>String</item-type>
<label>Charging State</label>
<description>The charging state (Start Charging, Smart Charging, Stop Charging).</description>
<state>
<options>
<option value="start">Start Charging</option>
<option value="smart">Smart Charging</option>
<option value="stop">Stop Charging</option>
</options>
</state>
<autoUpdatePolicy>recommend</autoUpdatePolicy>
</channel-type>
<channel-type id="state">
<item-type>String</item-type>
<label>Device State</label>
<description>This is the current device state (Available, Plugged-In, Charging, Error, Disconnected).</description>
<state readOnly="true">
<options>
<option value="standby">Available</option>
<option value="plugged">Plugged-In</option>
<option value="charging">Charging</option>
<option value="error">Error</option>
<option value="disconnect">Disconnected</option>
</options>
</state>
</channel-type>
<channel-type id="message">
<item-type>String</item-type>
<label>State Message</label>
<description>This is a message detailing the state of the EV charger.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="override">
<item-type>Switch</item-type>
<label>Override State</label>
<description>Smart charging is overridden.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="chargingTimeLeft">
<item-type>Number:Time</item-type>
<label>Charging Time Left</label>
<description>Charging time left.</description>
<category>Time</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="plugUnplugTime">
<item-type>DateTime</item-type>
<label>Plug/Unplug Time</label>
<description>Last time of either plug-in or plug-out.</description>
<category>Time</category>
<state readOnly="true" pattern="%1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp"/>
</channel-type>
<channel-type id="targetTime">
<item-type>DateTime</item-type>
<label>Target Time</label>
<description>“Start charging” start time, or time to start when overriding smart charging.</description>
<category>Time</category>
<state pattern="%1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp"/>
</channel-type>
<channel-type id="unitTime">
<item-type>DateTime</item-type>
<label>Unit Time</label>
<description>Current time on the unit.</description>
<category>Time</category>
<state readOnly="true" pattern="%1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp"/>
</channel-type>
<channel-type id="temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Current temperature at the unit.</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="currentLimit">
<item-type>Number:ElectricCurrent</item-type>
<label>Current Limit</label>
<description>Max charging current allowed.</description>
<state pattern="%d %unit%"/>
</channel-type>
<channel-type id="current">
<item-type>Number:ElectricCurrent</item-type>
<label>Current</label>
<description>Current charging current.</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="voltage">
<item-type>Number:ElectricPotential</item-type>
<label>Voltage</label>
<description>Current voltage.</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="energy">
<item-type>Number:Energy</item-type>
<label>Current Energy</label>
<description>Current power level of vehicle.</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="savings">
<item-type>Number</item-type>
<label>Savings</label>
<description>Current session EV savings.</description>
<state readOnly="true" pattern="$%.2f"/>
</channel-type>
<channel-type id="power">
<item-type>Number:Power</item-type>
<label>Charging Power</label>
<description>Current charging power.</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="chargingTime">
<item-type>Number:Time</item-type>
<label>Charging Time</label>
<description>Charging time since plug-in time.</description>
<category>Time</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="energyAtPlugin">
<item-type>Number:Energy</item-type>
<label>Energy at Plugin</label>
<description>Energy value at the plugging time.</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="energyToAdd">
<item-type>Number:Energy</item-type>
<label>Energy to Add</label>
<description>Amount of energy to be added in current session.</description>
<state pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="lifetimeEnergy">
<item-type>Number:Energy</item-type>
<label>Lifetime Energy</label>
<description>Total energy delivered to vehicles during lifetime.</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="lifetimeSavings">
<item-type>Number</item-type>
<label>Lifetime Savings</label>
<description>EV driving saving during lifetime.</description>
<state readOnly="true" pattern="$%.2f"/>
</channel-type>
<channel-type id="gasCost">
<item-type>Number</item-type>
<label>Gas Cost</label>
<description>Cost of gasoline used in savings calculations.</description>
<state readOnly="true" pattern="$%.2f"/>
</channel-type>
<channel-type id="fuelConsumption">
<item-type>Number</item-type>
<label>Fuel consumption</label>
<description>Distance per volume (mpg) used in savings calculations.</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="ecost">
<item-type>Number</item-type>
<label>Utility Energy Cost</label>
<description>Cost of electricity from utility company. (currency / kWh)</description>
<state readOnly="true" pattern="$%.2f"/>
</channel-type>
<channel-type id="energyPerMile">
<item-type>Number:Power</item-type>
<label>Energy Hours Per Mile</label>
<description>Energy Hours Per Mile.</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="carDescription">
<item-type>String</item-type>
<label>Car Description</label>
<description>Car description of vehicle currently or last charged.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="carBatterySize">
<item-type>Number:Energy</item-type>
<label>Car Battery Pack Size</label>
<description>Car battery pack size.</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="carBatteryRange">
<item-type>Number:Length</item-type>
<label>Mileage Range</label>
<description>Car distance range.</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="carChargingRate">
<item-type>Number:Power</item-type>
<label>Car Charging Rate</label>
<description>Car charging rate.</description>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -186,6 +186,7 @@
<module>org.openhab.binding.jablotron</module>
<module>org.openhab.binding.jeelink</module>
<module>org.openhab.binding.jellyfin</module>
<module>org.openhab.binding.juicenet</module>
<module>org.openhab.binding.kaleidescape</module>
<module>org.openhab.binding.keba</module>
<module>org.openhab.binding.km200</module>