diff --git a/CODEOWNERS b/CODEOWNERS
index 3f7ad568d..34634bf49 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 06e5a0ac7..d5178fa3b 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -761,6 +761,11 @@
org.openhab.binding.jellyfin
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.juicenet
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.kaleidescape
diff --git a/bundles/org.openhab.binding.juicenet/NOTICE b/bundles/org.openhab.binding.juicenet/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/NOTICE
@@ -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
diff --git a/bundles/org.openhab.binding.juicenet/README.md b/bundles/org.openhab.binding.juicenet/README.md
new file mode 100644
index 000000000..807a05173
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/README.md
@@ -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.
+
+
+
+```
+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
+```
+
diff --git a/bundles/org.openhab.binding.juicenet/doc/widget.png b/bundles/org.openhab.binding.juicenet/doc/widget.png
new file mode 100644
index 000000000..708e5b413
Binary files /dev/null and b/bundles/org.openhab.binding.juicenet/doc/widget.png differ
diff --git a/bundles/org.openhab.binding.juicenet/pom.xml b/bundles/org.openhab.binding.juicenet/pom.xml
new file mode 100644
index 000000000..70ab55745
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/pom.xml
@@ -0,0 +1,16 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.4.0-SNAPSHOT
+
+
+ org.openhab.binding.juicenet
+
+ openHAB Add-ons :: Bundles :: JuiceNet Binding
+
diff --git a/bundles/org.openhab.binding.juicenet/src/main/docs/JuiceNet API_client_12_11_2017.docx b/bundles/org.openhab.binding.juicenet/src/main/docs/JuiceNet API_client_12_11_2017.docx
new file mode 100644
index 000000000..ff9a2ae37
Binary files /dev/null and b/bundles/org.openhab.binding.juicenet/src/main/docs/JuiceNet API_client_12_11_2017.docx differ
diff --git a/bundles/org.openhab.binding.juicenet/src/main/feature/feature.xml b/bundles/org.openhab.binding.juicenet/src/main/feature/feature.xml
new file mode 100644
index 000000000..9dafc20b2
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.juicenet/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetBindingConstants.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetBindingConstants.java
new file mode 100644
index 000000000..13b5f5382
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetBindingConstants.java
@@ -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";
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetHandlerFactory.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetHandlerFactory.java
new file mode 100644
index 000000000..fafc0ae2e
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetHandlerFactory.java
@@ -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 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApi.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApi.java
new file mode 100644
index 000000000..667b5119d
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApi.java
@@ -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 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 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 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 params = new HashMap<>();
+
+ return postApiCommand(cmd, token, params);
+ }
+
+ public JsonObject postApiCommand(ApiCommand cmd, @Nullable String token, Map 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApiException.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApiException.java
new file mode 100644
index 000000000..29c8a4064
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApiException.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiCar.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiCar.java
new file mode 100644
index 000000000..b9abb023f
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiCar.java
@@ -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 = "";
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDevice.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDevice.java
new file mode 100644
index 000000000..bddb3b11c
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDevice.java
@@ -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 = "";
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceChargingStatus.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceChargingStatus.java
new file mode 100644
index 000000000..4f5f62251
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceChargingStatus.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceLifetimeStatus.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceLifetimeStatus.java
new file mode 100644
index 000000000..20f1f81fb
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceLifetimeStatus.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceStatus.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceStatus.java
new file mode 100644
index 000000000..80fe7ee09
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceStatus.java
@@ -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 = "";
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiInfo.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiInfo.java
new file mode 100644
index 000000000..78f65875b
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiInfo.java
@@ -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 = {};
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouDay.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouDay.java
new file mode 100644
index 000000000..d8e3a97ff
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouDay.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouSchedule.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouSchedule.java
new file mode 100644
index 000000000..7cb86fda5
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouSchedule.java
@@ -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();
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/config/JuiceNetBridgeConfiguration.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/config/JuiceNetBridgeConfiguration.java
new file mode 100644
index 000000000..893e004e3
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/config/JuiceNetBridgeConfiguration.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/discovery/JuiceNetDiscoveryService.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/discovery/JuiceNetDiscoveryService.java
new file mode 100644
index 000000000..8c015c848
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/discovery/JuiceNetDiscoveryService.java
@@ -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 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);
+ }
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetBridgeHandler.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetBridgeHandler.java
new file mode 100644
index 000000000..3ebb4e952
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetBridgeHandler.java
@@ -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> 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 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 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 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();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetDeviceHandler.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetDeviceHandler.java
new file mode 100644
index 000000000..333f832aa
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetDeviceHandler.java
@@ -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));
+ }
+}
diff --git a/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 000000000..681e833e9
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ JuiceNet Binding
+ This is the binding supporting the JuiceNet EV charger.
+
+
diff --git a/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/i18n/juicenet.properties b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/i18n/juicenet.properties
new file mode 100644
index 000000000..ea0bda216
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/i18n/juicenet.properties
@@ -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
diff --git a/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-account.xml b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-account.xml
new file mode 100644
index 000000000..40e76cf36
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-account.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ This is the account for which your device(s) are registered at home.juice.net.
+
+
+
+
+ API Token from the user profile page. (https://home.juice.net/Manage)
+
+
+
+ Interval the device is polled in seconds.
+ 60
+
+
+
+
diff --git a/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-device.xml b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-device.xml
new file mode 100644
index 000000000..e12493919
--- /dev/null
+++ b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-device.xml
@@ -0,0 +1,288 @@
+
+
+
+
+
+
+
+
+
+ JuiceBox EV Charger
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ unitID
+
+
+
+
+ EV charger Unit ID from the JuiceNet webpage. (https://home.juice.net)
+
+
+
+
+
+ String
+
+ Juice Box name.
+
+
+
+
+ String
+
+ The charging state (Start Charging, Smart Charging, Stop Charging).
+
+
+
+
+
+
+
+ recommend
+
+
+
+ String
+
+ This is the current device state (Available, Plugged-In, Charging, Error, Disconnected).
+
+
+
+
+
+
+
+
+
+
+
+
+ String
+
+ This is a message detailing the state of the EV charger.
+
+
+
+
+ Switch
+
+ Smart charging is overridden.
+
+
+
+
+ Number:Time
+
+ Charging time left.
+ Time
+
+
+
+
+ DateTime
+
+ Last time of either plug-in or plug-out.
+ Time
+
+
+
+
+ DateTime
+
+ “Start charging” start time, or time to start when overriding smart charging.
+ Time
+
+
+
+
+ DateTime
+
+ Current time on the unit.
+ Time
+
+
+
+
+ Number:Temperature
+
+ Current temperature at the unit.
+ Temperature
+
+
+
+
+ Number:ElectricCurrent
+
+ Max charging current allowed.
+
+
+
+
+ Number:ElectricCurrent
+
+ Current charging current.
+
+
+
+
+ Number:ElectricPotential
+
+ Current voltage.
+
+
+
+
+ Number:Energy
+
+ Current power level of vehicle.
+
+
+
+
+ Number
+
+ Current session EV savings.
+
+
+
+
+ Number:Power
+
+ Current charging power.
+
+
+
+
+ Number:Time
+
+ Charging time since plug-in time.
+ Time
+
+
+
+
+ Number:Energy
+
+ Energy value at the plugging time.
+
+
+
+
+ Number:Energy
+
+ Amount of energy to be added in current session.
+
+
+
+
+ Number:Energy
+
+ Total energy delivered to vehicles during lifetime.
+
+
+
+
+ Number
+
+ EV driving saving during lifetime.
+
+
+
+
+ Number
+
+ Cost of gasoline used in savings calculations.
+
+
+
+
+ Number
+
+ Distance per volume (mpg) used in savings calculations.
+
+
+
+
+ Number
+
+ Cost of electricity from utility company. (currency / kWh)
+
+
+
+
+ Number:Power
+
+ Energy Hours Per Mile.
+
+
+
+
+ String
+
+ Car description of vehicle currently or last charged.
+
+
+
+
+ Number:Energy
+
+ Car battery pack size.
+
+
+
+
+ Number:Length
+
+ Car distance range.
+
+
+
+
+ Number:Power
+
+ Car charging rate.
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 54fdac650..316b6a9ad 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -186,6 +186,7 @@
org.openhab.binding.jablotron
org.openhab.binding.jeelink
org.openhab.binding.jellyfin
+ org.openhab.binding.juicenet
org.openhab.binding.kaleidescape
org.openhab.binding.keba
org.openhab.binding.km200