]> git.basschouten.com Git - openhab-addons.git/commitdiff
[juicenet] Initial contribution (#10768)
authorjsjames <jeff@james-online.com>
Sun, 13 Nov 2022 11:27:43 +0000 (03:27 -0800)
committerGitHub <noreply@github.com>
Sun, 13 Nov 2022 11:27:43 +0000 (12:27 +0100)
Signed-off-by: Jeff James <jeff@james-online.com>
29 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.juicenet/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/README.md [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/doc/widget.png [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/docs/JuiceNet API_client_12_11_2017.docx [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApi.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApiException.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiCar.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDevice.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceChargingStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceLifetimeStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouDay.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouSchedule.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/config/JuiceNetBridgeConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/discovery/JuiceNetDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetDeviceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/i18n/juicenet.properties [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-account.xml [new file with mode: 0644]
bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-device.xml [new file with mode: 0644]
bundles/pom.xml

index 3f7ad568df44c274ba0646234ec9727dc0763deb..34634bf4910003118c0eef4e60c8052ca6b77031 100644 (file)
 /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
index 06e5a0ac739e4befe724afc093a981e9111d5442..d5178fa3bc06d7fa2d63973fda6cda6c2c2fdc29 100644 (file)
       <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>
diff --git a/bundles/org.openhab.binding.juicenet/NOTICE b/bundles/org.openhab.binding.juicenet/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..807a051
--- /dev/null
@@ -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
+```
+
diff --git a/bundles/org.openhab.binding.juicenet/doc/widget.png b/bundles/org.openhab.binding.juicenet/doc/widget.png
new file mode 100644 (file)
index 0000000..708e5b4
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 (file)
index 0000000..70ab557
--- /dev/null
@@ -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>
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 (file)
index 0000000..ff9a2ae
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 (file)
index 0000000..9dafc20
--- /dev/null
@@ -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>
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 (file)
index 0000000..13b5f53
--- /dev/null
@@ -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 (file)
index 0000000..fafc0ae
--- /dev/null
@@ -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;
+    }
+}
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 (file)
index 0000000..667b511
--- /dev/null
@@ -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;
+    }
+}
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 (file)
index 0000000..29c8a40
--- /dev/null
@@ -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 (file)
index 0000000..b9abb02
--- /dev/null
@@ -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 (file)
index 0000000..bddb3b1
--- /dev/null
@@ -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 (file)
index 0000000..4f5f622
--- /dev/null
@@ -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 (file)
index 0000000..20f1f81
--- /dev/null
@@ -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 (file)
index 0000000..80fe7ee
--- /dev/null
@@ -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 (file)
index 0000000..78f6587
--- /dev/null
@@ -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 (file)
index 0000000..d8e3a97
--- /dev/null
@@ -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 (file)
index 0000000..7cb86fd
--- /dev/null
@@ -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 (file)
index 0000000..893e004
--- /dev/null
@@ -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 (file)
index 0000000..8c015c8
--- /dev/null
@@ -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);
+    }
+}
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 (file)
index 0000000..3ebb4e9
--- /dev/null
@@ -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();
+        }
+    }
+}
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 (file)
index 0000000..333f832
--- /dev/null
@@ -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 (file)
index 0000000..681e833
--- /dev/null
@@ -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>
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 (file)
index 0000000..ea0bda2
--- /dev/null
@@ -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 (file)
index 0000000..40e76cf
--- /dev/null
@@ -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>
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 (file)
index 0000000..e124939
--- /dev/null
@@ -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>
index 54fdac650acc71d5842ad8bff6adc3f6e43a699e..316b6a9ad2d788a94d90c72140552a1f1714395a 100644 (file)
     <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>