diff --git a/CODEOWNERS b/CODEOWNERS index 3f7ad568d..34634bf49 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -153,6 +153,7 @@ /bundles/org.openhab.binding.jablotron/ @octa22 /bundles/org.openhab.binding.jeelink/ @vbier /bundles/org.openhab.binding.jellyfin/ @GiviMAD +/bundles/org.openhab.binding.juicenet/ @jsjames /bundles/org.openhab.binding.kaleidescape/ @mlobstein /bundles/org.openhab.binding.keba/ @kgoderis /bundles/org.openhab.binding.km200/ @Markinus diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 06e5a0ac7..d5178fa3b 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -761,6 +761,11 @@ org.openhab.binding.jellyfin ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.juicenet + ${project.version} + org.openhab.addons.bundles org.openhab.binding.kaleidescape diff --git a/bundles/org.openhab.binding.juicenet/NOTICE b/bundles/org.openhab.binding.juicenet/NOTICE new file mode 100644 index 000000000..38d625e34 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.juicenet/README.md b/bundles/org.openhab.binding.juicenet/README.md new file mode 100644 index 000000000..807a05173 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/README.md @@ -0,0 +1,270 @@ +# JuiceNet Binding + +The JuiceNet binding will interface with the cloud portal to get status and manage your JuiceBox EV charger(s). +In addition to getting the status of various items from the EV charger, it is also possible to start and stop charging sessions. + +## Supported Things + +This binding supports the following things: + +| thing | type | description | +|---------- |-------- |------------------------------ | +| JuiceNet Account | Bridge | This represents the cloud account to interface with the JuiceNet API. | +| JuiceBox EV Charger | Device | This interfaces to a specific JuiceBox EV charger associated with the JuiceNet account. | + +This binding should work with multiple JuiceBox EV chargers associated with the account, however it is currently only tested with a single EV charger. + +### Discovery + +Once a JuiceNet Account bridge has been created, any JuiceBox EV Chargers associated with this account will be discovered. + + +### Thing Configuration + +The configuration required is to create a JuiceNet account thing and fill in the appropriate API token. +The API token can be found on the Account page at https://home.juice.net/Manage. + +A JuiceBox EV Charger requires a a unitID which can also be found in the device settings at the JuiceNet web page. + +## Channels + +| channel | type | read-only | description | +|---------- |-------- |--------- | ------- | +| name | String | Y | Name of device.| +| chargingState | String | N | Current charging state (Start Charging, Smart Charging, Stop Charging). | +| state | String | Y | This is the current device state (Available, Plugged-In, Charging, Error, Disconnected). | +| message | String | Y | This is a message detailing the state of the EV charger. | +| override | Switch | Y | Smart charging is overridden. | +| chargingTimeLeft | Number:Time | Y | Charging time left (seconds). | +| plugUnplugTime | DateTime | Y | Last time of either plug-in or plug-out. | +| targetTime | DateTime | N | “Start charging” start time, or time to start when overriding smart charging. | +| unitTime | DateTime | Y | Current time on the unit. | +| temperature | Number:Temperature | Y | Current temperature at the unit. | +| currentLimit | Number:ElectricCurrent | N | Max charging current allowed. | +| current | Number:ElectricCurrent | Y | Current charging current. | +| voltage | Number:ElectricPotential | Y | Current voltage. | +| energy | Number:Energy | Y | Current amount of energy poured to the vehicle. | +| savings | Number | Y | Current session EV savings. | +| power | Number:Power | Y | Current charging power. | +| secondsCharging | Number:Time | Y | Charging time since plug-in time. | +| energyAtPlugin | Number:Energy | Y | Energy value at the plugging time. | +| energyToAdd | Number:Energy | N | Amount of energy to be added in current session. | +| lifetimeEnergy | Number:Energy | Y | Total energy delivered to vehicles during lifetime. | +| lifetimeSavings | Number | Y | EV driving saving during lifetime. | +| gasCost | Number | Y | Cost of gasoline used in savings calculations. | +| fuelConsumption | Number | Y | Miles per gallon used in savings calculations. | +| ecost | Number | Y | Cost of electricity from utility company. (currency/kWh) | +| energyPerMile | Number | Y | Energy per mile. | +| carDescription | String | Y | Car description of vehicle currently or last charged. | +| carBatterySize | Number:Energy | Y | Car battery pack size. | +| carBatteryRange | Number:Length | Y | Car range. | +| carChargingRate | Number:Power | Y | Car charging rate. | + +## Full Example + +### Things File + +If configuring the binding with manual configuration an example thing file looks like this: + +``` +Bridge juicenet:account:myaccount [ apiToken="xxxx-xxxx-xxxx-xxxx-xxxxx" ] { + Thing device JamesCharger [ unitID="xxxxxxx" ] +} +``` + +### Items File + +An example of an items file is here. + +``` +String JuiceNet_Name "Name" { channel="juicenet:device:myaccount:JamesCharger:name" } +String JuiceNet_State "Device State" { channel="juicenet:device:myaccount:JamesCharger:state" } +String JuiceNet_ChargingState "Charging State" { channel="juicenet:device:myaccount:JamesCharger:chargingState" } +String JuiceNet_Message "State Message" { channel="juicenet:device:myaccount:JamesCharger:message" } +Switch JuiceNet_Override "Override State" { channel="juicenet:device:myaccount:JamesCharger:override" } +DateTime JuiceNet_PlutUnplugTime "Plug/Unplug Time [%1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp]" { channel="juicenet:device:myaccount:JamesCharger:plugUnplugTime" } +DateTime JuiceNet_TargetTime "Target Time [%1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp]" { channel="juicenet:device:myaccount:JamesCharger:targetTime" } +Number:Time JuiceNet_ChargingTimeLeft "Charging Time Left [%.0f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:chargingTimeLeft" } +DateTime JuiceNet_UnitTime "Unit Time [%1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp]" { channel="juicenet:device:myaccount:JamesCharger:unitTime" } +Number:Temperature JuiceNet_Temperature "Temperature [%.0f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:temperature" } +Number:ElectricCurrent JuiceNet_CurrentLimit "Current Limit [%d %unit%]" { channel="juicenet:device:myaccount:JamesCharger:currentLimit" } +Number:ElectricCurrent JuiceNet_Current "Current [%.1f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:current" } +Number:ElectricPotential JuiceNet_Voltage "Voltage [%d %unit%]" { channel="juicenet:device:myaccount:JamesCharger:voltage" } +Number:Energy JuiceNet_Energy "Current Energy [%.1f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:energy" } +Number:Power JuiceNet_Power "Charging Power [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:power" } +Number JuiceNet_Savings "Savings [$%.2f]" { channel="juicenet:device:myaccount:JamesCharger:savings" } +Number:Time JuiceNet_ChargingTime "Charging Time [%.0f %unit%]" { channel="jjuicenet:device:myaccount:JamesCharger:chargingTime" } +Number:Energy JuiceNet_EnergyToAdd "Energy to Add [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:energyToAdd" } +Number:Energy JuiceNet_EnergyAtPlugin "Energy at Plugin [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:energyAtPlugin" } +Number:Energy JuiceNet_LifetimeEnergy "Lifetime Energy [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:lifetimeEnergy" } +Number JuiceNet_GasCost "Gas Cost [$%.2f]" { channel="juicenet:device:myaccount:JamesCharger:gasCost" } +Number JuiceNet_FuelConsumption "Fuel consumption [%.1f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:fuelConsumption" } +Number JuiceNet_Ecost "Utility Energy Cost [$%.2f]" { channel="juicenet:device:myaccount:JamesCharger:ecost" } +Number JuiceNet_LifetimeSavings "Lifetime Savings [$%.2f]" { channel="juicenet:device:myaccount:JamesCharger:lifetimeSavings" } +Number:Power JuiceNet_EnergyPerMile "Energy Hours Per Mile [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:energyPerMile" } +String JuiceNet_CarDescription "Car Description" { channel="juicenet:device:myaccount:JamesCharger:carDescription" } +Number:Length JuiceNet_CarBatteryRange "Mileage Range [%d %unit%]" { channel="juicenet:device:myaccount:JamesCharger:carBatteryRange" } +Number:Energy JuiceNet_CarBatterySize "Car Battery Pack Size [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:carBatterySize" } +Number:Power JuiceNet_CarChargineRage "Car Charging Rate [%.2f %unit%]" { channel="juicenet:device:myaccount:JamesCharger:carChargingRate" } + +``` + +## Widget + +The following custom widget can be used with this binding. + +![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 index 000000000..708e5b413 Binary files /dev/null and b/bundles/org.openhab.binding.juicenet/doc/widget.png differ diff --git a/bundles/org.openhab.binding.juicenet/pom.xml b/bundles/org.openhab.binding.juicenet/pom.xml new file mode 100644 index 000000000..70ab55745 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/pom.xml @@ -0,0 +1,16 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.4.0-SNAPSHOT + + + org.openhab.binding.juicenet + + openHAB Add-ons :: Bundles :: JuiceNet Binding + diff --git a/bundles/org.openhab.binding.juicenet/src/main/docs/JuiceNet API_client_12_11_2017.docx b/bundles/org.openhab.binding.juicenet/src/main/docs/JuiceNet API_client_12_11_2017.docx new file mode 100644 index 000000000..ff9a2ae37 Binary files /dev/null and b/bundles/org.openhab.binding.juicenet/src/main/docs/JuiceNet API_client_12_11_2017.docx differ diff --git a/bundles/org.openhab.binding.juicenet/src/main/feature/feature.xml b/bundles/org.openhab.binding.juicenet/src/main/feature/feature.xml new file mode 100644 index 000000000..9dafc20b2 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.juicenet/${project.version} + + diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetBindingConstants.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetBindingConstants.java new file mode 100644 index 000000000..13b5f5382 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetBindingConstants.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link JuiceNetBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetBindingConstants { + private static final String BINDING_ID = "juicenet"; + + // List of Bridge Type + public static final String BRIDGE = "account"; + + // List of all Device Types + public static final String DEVICE = "device"; + + // List of all Bridge Thing Type UIDs + public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, BRIDGE); + + // List of all Thing Type UIDs + public static final ThingTypeUID DEVICE_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE); + + // Device config parameter + public static final String PARAMETER_UNIT_ID = "unitID"; + + // Device properties + public static final String PROPERTY_NAME = "name"; + + // List of all Channel ids + public static final String CHANNEL_NAME = "name"; + public static final String CHANNEL_CHARGING_STATE = "chargingState"; + public static final String CHANNEL_STATE = "state"; + public static final String CHANNEL_MESSAGE = "message"; + public static final String CHANNEL_OVERRIDE = "override"; + public static final String CHANNEL_CHARGING_TIME_LEFT = "chargingTimeLeft"; + public static final String CHANNEL_PLUG_UNPLUG_TIME = "plugUnplugTime"; + public static final String CHANNEL_TARGET_TIME = "targetTime"; + public static final String CHANNEL_UNIT_TIME = "unitTime"; + public static final String CHANNEL_TEMPERATURE = "temperature"; + public static final String CHANNEL_CURRENT_LIMIT = "currentLimit"; + public static final String CHANNEL_CURRENT = "current"; + public static final String CHANNEL_VOLTAGE = "voltage"; + public static final String CHANNEL_ENERGY = "energy"; + public static final String CHANNEL_SAVINGS = "savings"; + public static final String CHANNEL_POWER = "power"; + public static final String CHANNEL_CHARGING_TIME = "chargingTime"; + public static final String CHANNEL_ENERGY_AT_PLUGIN = "energyAtPlugin"; + public static final String CHANNEL_ENERGY_TO_ADD = "energyToAdd"; + public static final String CHANNEL_LIFETIME_ENERGY = "lifetimeEnergy"; + public static final String CHANNEL_LIFETIME_SAVINGS = "lifetimeSavings"; + + public static final String CHANNEL_GAS_COST = "gasCost"; + public static final String CHANNEL_FUEL_CONSUMPTION = "fuelConsumption"; + public static final String CHANNEL_ECOST = "ecost"; + public static final String CHANNEL_ENERGY_PER_MILE = "energyPerMile"; + + public static final String CHANNEL_CAR_DESCRIPTION = "carDescription"; + public static final String CHANNEL_CAR_BATTERY_SIZE = "carBatterySize"; + public static final String CHANNEL_CAR_BATTERY_RANGE = "carBatteryRange"; + public static final String CHANNEL_CAR_CHARGING_RATE = "carChargingRate"; +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetHandlerFactory.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetHandlerFactory.java new file mode 100644 index 000000000..fafc0ae2e --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/JuiceNetHandlerFactory.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal; + +import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.juicenet.internal.handler.JuiceNetBridgeHandler; +import org.openhab.binding.juicenet.internal.handler.JuiceNetDeviceHandler; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link JuiceNetHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.juicenet", service = ThingHandlerFactory.class) +public class JuiceNetHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(BRIDGE_THING_TYPE, DEVICE_THING_TYPE); + private final HttpClientFactory httpClientFactory; + private final TimeZoneProvider timeZoneProvider; + + @Activate + public JuiceNetHandlerFactory(@Reference HttpClientFactory httpClientFactory, + @Reference TimeZoneProvider timeZoneProvider) { + this.httpClientFactory = httpClientFactory; + this.timeZoneProvider = timeZoneProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(BRIDGE_THING_TYPE)) { + return new JuiceNetBridgeHandler((Bridge) thing, httpClientFactory.getCommonHttpClient()); + } else if (thingTypeUID.equals(DEVICE_THING_TYPE)) { + return new JuiceNetDeviceHandler(thing, timeZoneProvider); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApi.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApi.java new file mode 100644 index 000000000..667b5119d --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApi.java @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.api; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiDevice; +import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiDeviceStatus; +import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiInfo; +import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiTouSchedule; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link JuiceNetApi} is responsible for implementing the api interface to the JuiceNet cloud server + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetApi { + private final Logger logger = LoggerFactory.getLogger(JuiceNetApi.class); + + private static final String API_HOST = "https://jbv1-api.emotorwerks.com/"; + private static final String API_ACCOUNT = API_HOST + "box_pin"; + private static final String API_DEVICE = API_HOST + "box_api_secure"; + + private String apiToken = ""; + private HttpClient httpClient; + private ThingUID bridgeUID; + + public enum ApiCommand { + GET_ACCOUNT_UNITS("get_account_units", API_ACCOUNT), + GET_STATE("get_state", API_DEVICE), + SET_CHARGING_LIMIT("set_limit", API_DEVICE), + GET_SCHEDULE("get_schedule", API_DEVICE), + SET_SCHEDULE("set_schedule", API_DEVICE), + GET_INFO("get_info", API_DEVICE), + SET_OVERRIDE("set_override", API_DEVICE); + + final String command; + final String uri; + + ApiCommand(String command, String uri) { + this.command = command; + this.uri = uri; + } + } + + public JuiceNetApi(HttpClient httpClient, ThingUID bridgeUID) { + this.bridgeUID = bridgeUID; + this.httpClient = httpClient; + } + + public void setApiToken(String apiToken) { + this.apiToken = apiToken; + } + + public List queryDeviceList() throws JuiceNetApiException, InterruptedException { + JuiceNetApiDevice[] listDevices; + try { + JsonObject jsonResponse = postApiCommand(ApiCommand.GET_ACCOUNT_UNITS, null); + + JsonElement unitsElement = jsonResponse.get("units"); + if (unitsElement == null) { + throw new JuiceNetApiException("getDevices from Juicenet API failed, no 'units' element in response."); + } + + listDevices = new Gson().fromJson(unitsElement.getAsJsonArray(), JuiceNetApiDevice[].class); + } catch (JsonSyntaxException e) { + throw new JuiceNetApiException("getDevices from JuiceNet API failed, invalid JSON list."); + } catch (IllegalStateException e) { + throw new JuiceNetApiException("getDevices from JuiceNet API failed - did not return valid array."); + } + + return Arrays.asList(listDevices); + } + + public JuiceNetApiDeviceStatus queryDeviceStatus(String token) throws JuiceNetApiException, InterruptedException { + JuiceNetApiDeviceStatus deviceStatus; + try { + JsonObject jsonResponse = postApiCommand(ApiCommand.GET_STATE, token); + + deviceStatus = new Gson().fromJson(jsonResponse, JuiceNetApiDeviceStatus.class); + } catch (JsonSyntaxException e) { + throw new JuiceNetApiException("queryDeviceStatus from JuiceNet API failed, invalid JSON list."); + } catch (IllegalStateException e) { + throw new JuiceNetApiException("queryDeviceStatus from JuiceNet API failed - did not return valid array."); + } + + return Objects.requireNonNull(deviceStatus); + } + + public JuiceNetApiInfo queryInfo(String token) throws InterruptedException, JuiceNetApiException { + JuiceNetApiInfo info; + try { + JsonObject jsonResponse = postApiCommand(ApiCommand.GET_INFO, token); + + info = new Gson().fromJson(jsonResponse, JuiceNetApiInfo.class); + } catch (JsonSyntaxException e) { + throw new JuiceNetApiException("queryInfo from JuiceNet API failed, invalid JSON list."); + } catch (IllegalStateException e) { + throw new JuiceNetApiException("queryInfo from JuiceNet API failed - did not return valid array."); + } + + return Objects.requireNonNull(info); + } + + public JuiceNetApiTouSchedule queryTOUSchedule(String token) throws InterruptedException, JuiceNetApiException { + JuiceNetApiTouSchedule deviceTouSchedule; + try { + JsonObject jsonResponse = postApiCommand(ApiCommand.GET_SCHEDULE, token); + + deviceTouSchedule = new Gson().fromJson(jsonResponse, JuiceNetApiTouSchedule.class); + } catch (JsonSyntaxException e) { + throw new JuiceNetApiException("queryTOUSchedule from JuiceNet API failed, invalid JSON list."); + } catch (IllegalStateException e) { + throw new JuiceNetApiException("queryTOUSchedule from JuiceNet API failed - did not return valid array."); + } + + return Objects.requireNonNull(deviceTouSchedule); + } + + public void setOverride(String token, int energy_at_plugin, Long override_time, int energy_to_add) + throws InterruptedException, JuiceNetApiException { + Map params = new HashMap<>(); + + params.put("energy_at_plugin", Integer.toString(energy_at_plugin)); + params.put("override_time", Long.toString(energy_at_plugin)); + params.put("energy_to_add", Integer.toString(energy_at_plugin)); + + postApiCommand(ApiCommand.SET_OVERRIDE, token, params); + } + + public void setCurrentLimit(String token, int limit) throws InterruptedException, JuiceNetApiException { + Map params = new HashMap<>(); + + params.put("amperage", Integer.toString(limit)); + + postApiCommand(ApiCommand.SET_OVERRIDE, token, params); + } + + public JsonObject postApiCommand(ApiCommand cmd, @Nullable String token) + throws InterruptedException, JuiceNetApiException { + Map params = new HashMap<>(); + + return postApiCommand(cmd, token, params); + } + + public JsonObject postApiCommand(ApiCommand cmd, @Nullable String token, Map params) + throws InterruptedException, JuiceNetApiException { + Request request = httpClient.POST(cmd.uri); + request.header(HttpHeader.CONTENT_TYPE, "application/json"); + + // Add required params + params.put("cmd", cmd.command); + params.put("device_id", bridgeUID.getAsString()); + params.put("account_token", apiToken); + + if (token != null) { + params.put("token", token); + } + + JsonObject jsonResponse; + try { + request.content(new StringContentProvider(new Gson().toJson(params)), "application/json"); + ContentResponse response = request.send(); + if (response.getStatus() != HttpStatus.OK_200) { + throw new JuiceNetApiException( + cmd.command + "from JuiceNet API unsucessful, please check configuation. (HTTP code :" + + response.getStatus() + ")."); + } + + String responseString = response.getContentAsString(); + logger.trace("{}", responseString); + + jsonResponse = JsonParser.parseString(responseString).getAsJsonObject(); + JsonElement successElement = jsonResponse.get("success"); + if (successElement == null) { + throw new JuiceNetApiException( + cmd.command + " from JuiceNet API failed, 'success' element missing from response."); + } + boolean success = successElement.getAsBoolean(); + + if (!success) { + throw new JuiceNetApiException(cmd.command + " from JuiceNet API failed, please check configuration."); + } + } catch (IllegalStateException e) { + throw new JuiceNetApiException(cmd.command + " from JuiceNet API failed, invalid JSON."); + } catch (TimeoutException e) { + throw new JuiceNetApiException(cmd.command + " from JuiceNet API timeout."); + } catch (ExecutionException e) { + throw new JuiceNetApiException(cmd.command + " from JuiceNet API execution issue."); + } + + return jsonResponse; + } +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApiException.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApiException.java new file mode 100644 index 000000000..29c8a4064 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/JuiceNetApiException.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link JuiceNetApiException} implements an API Exception + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetApiException extends Exception { + private static final long serialVersionUID = 5421236828224242152L; + + public JuiceNetApiException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiCar.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiCar.java new file mode 100644 index 000000000..b9abb023f --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiCar.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * {@link JuiceNetApiCar } implements DTO for Car API call + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetApiCar { + @SerializedName("car_id") + public int carId; + public String description = ""; + @SerializedName("battery_size_wh") + public int batterySizeWH; + @SerializedName("battery_range_m") + public int batteryRangeM; + @SerializedName("charging_rate_w") + public int chargingRateW; + @SerializedName("model_id") + public String modelId = ""; +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDevice.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDevice.java new file mode 100644 index 000000000..bddb3b11c --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDevice.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * {@link JuiceNetApiDevice } implements DTO for Device Info API call + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetApiDevice { + public String name = ""; + public String token = ""; + @SerializedName("unit_id") + public String unitId = ""; +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceChargingStatus.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceChargingStatus.java new file mode 100644 index 000000000..4f5f62251 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceChargingStatus.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * {@link JuiceNetDeviceChargingStatus } implements DTO for device charging status + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetApiDeviceChargingStatus { + @SerializedName("amps_limit") + public int ampsLimit; + @SerializedName("amps_current") + public float ampsCurrent; + public int voltage; + @SerializedName("wh_energy") + public int whEnergy; + public int savings; + @SerializedName("watt_power") + public int wattPower; + @SerializedName("seconds_charging") + public int secondsCharging; + @SerializedName("wh_energy_at_plugin") + public int whEnergyAtPlugin; + @SerializedName("wh_energy_to_add") + public int whEnergyToAdd; + public int flags; +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceLifetimeStatus.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceLifetimeStatus.java new file mode 100644 index 000000000..20f1f81fb --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceLifetimeStatus.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * {@link JuiceNetApiDeviceLifetimeStatus } implements DTO for Device Lifetime Status + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetApiDeviceLifetimeStatus { + @SerializedName("wh_energy") + public int whEnergy; + public int savings; +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceStatus.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceStatus.java new file mode 100644 index 000000000..80fe7ee09 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiDeviceStatus.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * {@link JuiceNetApiDeviceStatus } implements DTO for Device Status + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetApiDeviceStatus { + @SerializedName("ID") + public String id = ""; + @SerializedName("info_timestamp") + public Long infoTimestamp = (long) 0; + @SerializedName("show_override") + public boolean showOverride; + public String state = ""; + public JuiceNetApiDeviceChargingStatus charging = new JuiceNetApiDeviceChargingStatus(); + public JuiceNetApiDeviceLifetimeStatus lifetime = new JuiceNetApiDeviceLifetimeStatus(); + @SerializedName("charging_time_left") + public int chargingTimeLeft; + @SerializedName("plug_unplug_time") + public Long plugUnplugTime = (long) 0; + @SerializedName("target_time") + public Long targetTime = (long) 0; + @SerializedName("unit_time") + public Long unitTime = (long) 0; + @SerializedName("utc_time") + public Long utcTime = (long) 0; + @SerializedName("default_target_time") + public long defaultTargetTime = 0; + @SerializedName("car_id") + public int carId; + public int temperature; + public String message = ""; +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiInfo.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiInfo.java new file mode 100644 index 000000000..78f65875b --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiInfo.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * {@link JuiceNetApiInfo } implements DTO for Info + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetApiInfo { + public String name = ""; + public String address = ""; + public String city = ""; + public String zip = ""; + @SerializedName("country_code") + public String countryCode = ""; + public String ip = ""; + @SerializedName("gascost") + public int gasCost; + public int mpg; + public int ecost; + @SerializedName("whpermile") + public int whPerMile; + public String timeZoneId = ""; + @SerializedName("amps_wire_rating") + public int ampsWireRating; + @SerializedName("amps_unit_rating") + public int ampsUnitRating; + public JuiceNetApiCar[] cars = {}; +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouDay.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouDay.java new file mode 100644 index 000000000..d8e3a97ff --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouDay.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * {@link JuiceNetApiTouDay } implements DTO for TOU settings + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetApiTouDay { + public int start; + public int end; + @SerializedName("car_ready_by") + public int carReadyBy; +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouSchedule.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouSchedule.java new file mode 100644 index 000000000..7cb86fda5 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/api/dto/JuiceNetApiTouSchedule.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.api.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link JuiceNetApiTouSchedule } implements DTO for TOU schedule + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetApiTouSchedule { + public String type = ""; + public JuiceNetApiTouDay weekday = new JuiceNetApiTouDay(); + public JuiceNetApiTouDay weenend = new JuiceNetApiTouDay(); +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/config/JuiceNetBridgeConfiguration.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/config/JuiceNetBridgeConfiguration.java new file mode 100644 index 000000000..893e004e3 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/config/JuiceNetBridgeConfiguration.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link JuiceNetBridgeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetBridgeConfiguration { + public String apiToken = ""; + public int refreshInterval = 60; +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/discovery/JuiceNetDiscoveryService.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/discovery/JuiceNetDiscoveryService.java new file mode 100644 index 000000000..8c015c848 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/discovery/JuiceNetDiscoveryService.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.discovery; + +import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*; + +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.juicenet.internal.handler.JuiceNetBridgeHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link JuiceNetDiscoveryService} discovers all devices/zones reported by the FlumeTech Cloud. This requires the + * api + * key to get access to the cloud data. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetDiscoveryService extends AbstractDiscoveryService + implements DiscoveryService, ThingHandlerService { + private final Logger logger = LoggerFactory.getLogger(JuiceNetDiscoveryService.class); + + private @Nullable JuiceNetBridgeHandler bridgeHandler; + + private static final Set DISCOVERABLE_THING_TYPES_UIDS = Set.of(DEVICE_THING_TYPE); + + public JuiceNetDiscoveryService() { + super(DISCOVERABLE_THING_TYPES_UIDS, 0, false); + } + + @Override + public void activate() { + super.activate(null); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + protected synchronized void startScan() { + Objects.requireNonNull(bridgeHandler).iterateApiDevices(); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof JuiceNetBridgeHandler) { + JuiceNetBridgeHandler bridgeHandler = (JuiceNetBridgeHandler) handler; + bridgeHandler.setDiscoveryService(this); + this.bridgeHandler = bridgeHandler; + } else { + this.bridgeHandler = null; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return this.bridgeHandler; + } + + public void notifyDiscoveryDevice(String id, String name) { + JuiceNetBridgeHandler bridgeHandler = this.bridgeHandler; + Objects.requireNonNull(bridgeHandler, "Discovery with null bridgehandler."); + ThingUID bridgeUID = bridgeHandler.getThing().getUID(); + + ThingUID uid = new ThingUID(DEVICE_THING_TYPE, bridgeUID, id); + + DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID) + .withProperty(PARAMETER_UNIT_ID, id).withLabel(name).build(); + thingDiscovered(result); + logger.debug("Discovered JuiceNetDevice {} - {}", uid, name); + } +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetBridgeHandler.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetBridgeHandler.java new file mode 100644 index 000000000..3ebb4e952 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetBridgeHandler.java @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.handler; + +import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.juicenet.internal.api.JuiceNetApi; +import org.openhab.binding.juicenet.internal.api.JuiceNetApiException; +import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiDevice; +import org.openhab.binding.juicenet.internal.config.JuiceNetBridgeConfiguration; +import org.openhab.binding.juicenet.internal.discovery.JuiceNetDiscoveryService; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link JuiceNetBridgeHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(JuiceNetBridgeHandler.class); + + private JuiceNetBridgeConfiguration config = new JuiceNetBridgeConfiguration(); + private final JuiceNetApi api; + + public JuiceNetApi getApi() { + return api; + } + + private @Nullable ScheduledFuture pollingJob; + private @Nullable JuiceNetDiscoveryService discoveryService; + + public JuiceNetBridgeHandler(Bridge bridge, HttpClient httpClient) { + super(bridge); + + this.api = new JuiceNetApi(httpClient, getThing().getUID()); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void initialize() { + config = getConfigAs(JuiceNetBridgeConfiguration.class); + + logger.trace("Bridge initialized: {}", Objects.requireNonNull(getThing()).getUID()); + + api.setApiToken(config.apiToken); + + updateStatus(ThingStatus.UNKNOWN); + // Bridge will go online after the first successful API call in iterateApiDevices. iterateApiDevices will be + // called when a child device attempts to goOnline and needs to retrieve the api token + + pollingJob = scheduler.scheduleWithFixedDelay(this::pollDevices, 10, config.refreshInterval, TimeUnit.SECONDS); + + // Call here in order to discover any devices. + iterateApiDevices(); + } + + @Override + public void dispose() { + logger.debug("Handler disposed."); + ScheduledFuture pollingJob = this.pollingJob; + if (pollingJob != null) { + pollingJob.cancel(true); + this.pollingJob = null; + } + } + + public void setDiscoveryService(JuiceNetDiscoveryService discoveryService) { + this.discoveryService = discoveryService; + } + + @Override + public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) { + // Call here to set the Api Token for any newly initialized Child devices + iterateApiDevices(); + } + + /** + * Get the services registered for this bridge. Provides the discovery service. + */ + @Override + public Collection> getServices() { + return Collections.singleton(JuiceNetDiscoveryService.class); + } + + public void handleApiException(Exception e) { + if (e instanceof JuiceNetApiException) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.toString()); + } else if (e instanceof InterruptedException) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.toString()); + Thread.currentThread().interrupt(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.toString()); + } + } + + @Nullable + public Thing getThingById(String id) { + List childThings = getThing().getThings(); + + for (Thing childThing : childThings) { + Configuration configuration = childThing.getConfiguration(); + + String childId = configuration.get(PARAMETER_UNIT_ID).toString(); + + if (childId.equals(id)) { + return childThing; + } + } + + return null; + } + + // This function will query the list of devices from the API and then set the name/token in the child handlers. If a + // child does not exist, it will notify the Discovery service. If it is successful, it will ensure the bridge status + // is updated + // to ONLINE. + public void iterateApiDevices() { + List listDevices; + + try { + listDevices = api.queryDeviceList(); + } catch (JuiceNetApiException | InterruptedException e) { + handleApiException(e); + return; + } + + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + + JuiceNetDiscoveryService discoveryService = this.discoveryService; + for (JuiceNetApiDevice dev : listDevices) { + Thing childThing = getThingById(dev.unitId); + if (childThing == null) { + if (discoveryService != null) { + discoveryService.notifyDiscoveryDevice(dev.unitId, dev.name); + } + } else { + JuiceNetDeviceHandler childHandler = (JuiceNetDeviceHandler) childThing.getHandler(); + if (childHandler != null) { + childHandler.setNameAndToken(dev.name, dev.token); + } + } + } + } + + private void pollDevices() { + List things = getThing().getThings(); + + for (Thing t : things) { + if (!t.getThingTypeUID().equals(DEVICE_THING_TYPE)) { + continue; + } + + JuiceNetDeviceHandler handler = (JuiceNetDeviceHandler) t.getHandler(); + if (handler == null) { + logger.trace("no handler for thing: {}", t.getUID()); + continue; + } + + handler.queryDeviceStatusAndInfo(); + } + } +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetDeviceHandler.java b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetDeviceHandler.java new file mode 100644 index 000000000..333f832aa --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/java/org/openhab/binding/juicenet/internal/handler/JuiceNetDeviceHandler.java @@ -0,0 +1,360 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.juicenet.internal.handler; + +import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.juicenet.internal.api.JuiceNetApi; +import org.openhab.binding.juicenet.internal.api.JuiceNetApiException; +import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiCar; +import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiDeviceStatus; +import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiInfo; +import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiTouSchedule; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.ImperialUnits; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link JuiceNetDeviceHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class JuiceNetDeviceHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(JuiceNetDeviceHandler.class); + + private final TimeZoneProvider timeZoneProvider; + + // properties + private String name = ""; + + private String token = ""; + private long targetTimeTou = 0; + private long lastInfoTimestamp = 0; + + JuiceNetApiDeviceStatus deviceStatus = new JuiceNetApiDeviceStatus(); + JuiceNetApiInfo deviceInfo = new JuiceNetApiInfo(); + JuiceNetApiTouSchedule deviceTouSchedule = new JuiceNetApiTouSchedule(); + JuiceNetApiCar deviceCar = new JuiceNetApiCar(); + + public JuiceNetDeviceHandler(Thing thing, TimeZoneProvider timeZoneProvider) { + super(thing); + + this.timeZoneProvider = timeZoneProvider; + } + + public void setNameAndToken(String name, String token) { + logger.trace("setNameAndToken"); + this.token = token; + + if (!name.equals(this.name)) { + updateProperty(PROPERTY_NAME, name); + this.name = name; + } + + if (getThing().getStatus() != ThingStatus.ONLINE) { + goOnline(); + } + } + + @Override + public void initialize() { + logger.trace("Device initialized: {}", Objects.requireNonNull(getThing().getUID())); + Configuration configuration = getThing().getConfiguration(); + + String stringId = configuration.get(PARAMETER_UNIT_ID).toString(); + if (stringId.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "@text/offline.configuration-error.id-missing"); + return; + } + + updateStatus(ThingStatus.UNKNOWN); + + // This device will go ONLINE on the first successful API call in queryDeviceStatusAndInfo + } + + private void handleApiException(Exception e) { + if (e instanceof JuiceNetApiException) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.toString()); + } else if (e instanceof InterruptedException) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.toString()); + Thread.currentThread().interrupt(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.toString()); + } + } + + private void goOnline() { + logger.trace("goOnline"); + if (this.getThing().getStatus() == ThingStatus.ONLINE) { + return; + } + + if (token.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "@text/offline.configuration-error.non-existent-device"); + return; + } + + try { + tryQueryDeviceStatusAndInfo(); + } catch (JuiceNetApiException | InterruptedException e) { + handleApiException(e); + return; + } + + updateStatus(ThingStatus.ONLINE); + } + + @Nullable + private JuiceNetApi getApi() { + Bridge bridge = getBridge(); + if (bridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "@text/offline.configuration-error.bridge-missing"); + return null; + } + BridgeHandler handler = Objects.requireNonNull(bridge.getHandler()); + + return ((JuiceNetBridgeHandler) handler).getApi(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + JuiceNetApi api = getApi(); + if (api == null) { + return; + } + + if (command instanceof RefreshType) { + switch (channelUID.getId()) { + case CHANNEL_NAME: + case CHANNEL_STATE: + case CHANNEL_MESSAGE: + case CHANNEL_OVERRIDE: + case CHANNEL_CHARGING_TIME_LEFT: + case CHANNEL_PLUG_UNPLUG_TIME: + case CHANNEL_TARGET_TIME: + case CHANNEL_UNIT_TIME: + case CHANNEL_TEMPERATURE: + case CHANNEL_CURRENT_LIMIT: + case CHANNEL_CURRENT: + case CHANNEL_VOLTAGE: + case CHANNEL_ENERGY: + case CHANNEL_SAVINGS: + case CHANNEL_POWER: + case CHANNEL_CHARGING_TIME: + case CHANNEL_ENERGY_AT_PLUGIN: + case CHANNEL_ENERGY_TO_ADD: + case CHANNEL_LIFETIME_ENERGY: + case CHANNEL_LIFETIME_SAVINGS: + case CHANNEL_CAR_DESCRIPTION: + case CHANNEL_CAR_BATTERY_SIZE: + case CHANNEL_CAR_BATTERY_RANGE: + case CHANNEL_CAR_CHARGING_RATE: + refreshStatusChannels(); + break; + case CHANNEL_GAS_COST: + case CHANNEL_FUEL_CONSUMPTION: + case CHANNEL_ECOST: + case CHANNEL_ENERGY_PER_MILE: + refreshInfoChannels(); + break; + } + + return; + } + + try { + switch (channelUID.getId()) { + case CHANNEL_CURRENT_LIMIT: + int limit = ((QuantityType) command).intValue(); + api.setCurrentLimit(Objects.requireNonNull(token), limit); + break; + case CHANNEL_TARGET_TIME: { + int energyAtPlugin = 0; + int energyToAdd = deviceCar.batterySizeWH; + + if (!(command instanceof DateTimeType)) { + logger.info("Target Time is not an instance of DateTimeType"); + return; + } + + ZonedDateTime datetime = ((DateTimeType) command).getZonedDateTime(); + Long targetTime = datetime.toEpochSecond() + datetime.get(ChronoField.OFFSET_SECONDS); + logger.debug("DateTime: {} - {}", datetime.toString(), targetTime); + + api.setOverride(Objects.requireNonNull(token), energyAtPlugin, targetTime, energyToAdd); + + break; + } + case CHANNEL_CHARGING_STATE: { + String state = ((StringType) command).toString(); + Long overrideTime = deviceStatus.unitTime; + int energyAtPlugin = 0; + int energyToAdd = deviceCar.batterySizeWH; + + switch (state) { + case "stop": + if (targetTimeTou == 0) { + targetTimeTou = deviceStatus.targetTime; + } + overrideTime = deviceStatus.unitTime + 31556926; + break; + case "start": + if (targetTimeTou == 0) { + targetTimeTou = deviceStatus.targetTime; + } + overrideTime = deviceStatus.unitTime; + break; + case "smart": + overrideTime = deviceStatus.defaultTargetTime; + break; + } + + api.setOverride(Objects.requireNonNull(token), energyAtPlugin, overrideTime, energyToAdd); + + break; + } + } + } catch (JuiceNetApiException | InterruptedException e) { + handleApiException(e); + return; + } + } + + private void tryQueryDeviceStatusAndInfo() throws JuiceNetApiException, InterruptedException { + String apiToken = Objects.requireNonNull(this.token); + JuiceNetApi api = getApi(); + if (api == null) { + return; + } + + deviceStatus = api.queryDeviceStatus(apiToken); + + if (deviceStatus.infoTimestamp > lastInfoTimestamp) { + lastInfoTimestamp = deviceStatus.infoTimestamp; + + deviceInfo = api.queryInfo(apiToken); + deviceTouSchedule = api.queryTOUSchedule(apiToken); + refreshInfoChannels(); + } + + int carId = deviceStatus.carId; + for (JuiceNetApiCar car : deviceInfo.cars) { + if (car.carId == carId) { + this.deviceCar = car; + break; + } + } + + refreshStatusChannels(); + } + + public void queryDeviceStatusAndInfo() { + logger.trace("queryStatusAndInfo"); + ThingStatus status = getThing().getStatus(); + + if (status != ThingStatus.ONLINE) { + goOnline(); + return; + } + + try { + tryQueryDeviceStatusAndInfo(); + } catch (JuiceNetApiException | InterruptedException e) { + handleApiException(e); + return; + } + } + + private ZonedDateTime toZonedDateTime(long localEpochSeconds) { + return Instant.ofEpochSecond(localEpochSeconds).atZone(timeZoneProvider.getTimeZone()); + } + + private void refreshStatusChannels() { + updateState(CHANNEL_STATE, new StringType(deviceStatus.state)); + + if (deviceStatus.targetTime <= deviceStatus.unitTime) { + updateState(CHANNEL_CHARGING_STATE, new StringType("start")); + } else if ((deviceStatus.targetTime - deviceStatus.unitTime) < TimeUnit.DAYS.toSeconds(2)) { + updateState(CHANNEL_CHARGING_STATE, new StringType("smart")); + } else { + updateState(CHANNEL_CHARGING_STATE, new StringType("stop")); + } + + updateState(CHANNEL_MESSAGE, new StringType(deviceStatus.message)); + updateState(CHANNEL_OVERRIDE, OnOffType.from(deviceStatus.showOverride)); + updateState(CHANNEL_CHARGING_TIME_LEFT, new QuantityType<>(deviceStatus.chargingTimeLeft, Units.SECOND)); + updateState(CHANNEL_PLUG_UNPLUG_TIME, new DateTimeType(toZonedDateTime(deviceStatus.plugUnplugTime))); + updateState(CHANNEL_TARGET_TIME, new DateTimeType(toZonedDateTime(deviceStatus.targetTime))); + updateState(CHANNEL_UNIT_TIME, new DateTimeType(toZonedDateTime(deviceStatus.utcTime))); + updateState(CHANNEL_TEMPERATURE, new QuantityType<>(deviceStatus.temperature, SIUnits.CELSIUS)); + updateState(CHANNEL_CURRENT_LIMIT, new QuantityType<>(deviceStatus.charging.ampsLimit, Units.AMPERE)); + updateState(CHANNEL_CURRENT, new QuantityType<>(deviceStatus.charging.ampsCurrent, Units.AMPERE)); + updateState(CHANNEL_VOLTAGE, new QuantityType<>(deviceStatus.charging.voltage, Units.VOLT)); + updateState(CHANNEL_ENERGY, new QuantityType<>(deviceStatus.charging.whEnergy, Units.WATT_HOUR)); + updateState(CHANNEL_SAVINGS, new DecimalType(deviceStatus.charging.savings / 100.0)); + updateState(CHANNEL_POWER, new QuantityType<>(deviceStatus.charging.wattPower, Units.WATT)); + updateState(CHANNEL_CHARGING_TIME, new QuantityType<>(deviceStatus.charging.secondsCharging, Units.SECOND)); + updateState(CHANNEL_ENERGY_AT_PLUGIN, + new QuantityType<>(deviceStatus.charging.whEnergyAtPlugin, Units.WATT_HOUR)); + updateState(CHANNEL_ENERGY_TO_ADD, new QuantityType<>(deviceStatus.charging.whEnergyToAdd, Units.WATT_HOUR)); + updateState(CHANNEL_LIFETIME_ENERGY, new QuantityType<>(deviceStatus.lifetime.whEnergy, Units.WATT_HOUR)); + updateState(CHANNEL_LIFETIME_SAVINGS, new DecimalType(deviceStatus.lifetime.savings / 100.0)); + + // update Car items + updateState(CHANNEL_CAR_DESCRIPTION, new StringType(deviceCar.description)); + updateState(CHANNEL_CAR_BATTERY_SIZE, new QuantityType<>(deviceCar.batterySizeWH, Units.WATT_HOUR)); + updateState(CHANNEL_CAR_BATTERY_RANGE, new QuantityType<>(deviceCar.batteryRangeM, ImperialUnits.MILE)); + updateState(CHANNEL_CAR_CHARGING_RATE, new QuantityType<>(deviceCar.chargingRateW, Units.WATT)); + } + + private void refreshInfoChannels() { + updateState(CHANNEL_NAME, new StringType(name)); + updateState(CHANNEL_GAS_COST, new DecimalType(deviceInfo.gasCost / 100.0)); + // currently there is no unit defined for fuel consumption + updateState(CHANNEL_FUEL_CONSUMPTION, new DecimalType(deviceInfo.mpg)); + updateState(CHANNEL_ECOST, new DecimalType(deviceInfo.ecost / 100.0)); + updateState(CHANNEL_ENERGY_PER_MILE, new DecimalType(deviceInfo.whPerMile)); + } +} diff --git a/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 000000000..681e833e9 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + JuiceNet Binding + This is the binding supporting the JuiceNet EV charger. + + diff --git a/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/i18n/juicenet.properties b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/i18n/juicenet.properties new file mode 100644 index 000000000..ea0bda216 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/i18n/juicenet.properties @@ -0,0 +1,98 @@ +# binding + +binding.juicenet.name = JuiceNet Binding +binding.juicenet.description = This is the binding supporting the JuiceNet EV charger. + +# thing types + +thing-type.juicenet.account.label = JuiceNet Account +thing-type.juicenet.account.description = This is the account for which your device(s) are registered at home.juice.net. +thing-type.juicenet.device.label = JuiceBox Charger +thing-type.juicenet.device.description = JuiceBox EV Charger + +# thing types config + +thing-type.config.juicenet.account.apiToken.label = API Token +thing-type.config.juicenet.account.apiToken.description = API Token from the user profile page. (https://home.juice.net/Manage) +thing-type.config.juicenet.account.refreshInterval.label = Refresh Interval +thing-type.config.juicenet.account.refreshInterval.description = Interval the device is polled in seconds. +thing-type.config.juicenet.device.unitID.label = Unit ID +thing-type.config.juicenet.device.unitID.description = EV charger Unit ID from the JuiceNet webpage. (https://home.juice.net) + +# channel types + +channel-type.juicenet.carBatteryRange.label = Mileage Range +channel-type.juicenet.carBatteryRange.description = Car distance range. +channel-type.juicenet.carBatterySize.label = Car Battery Pack Size +channel-type.juicenet.carBatterySize.description = Car battery pack size. +channel-type.juicenet.carChargingRate.label = Car Charging Rate +channel-type.juicenet.carChargingRate.description = Car charging rate. +channel-type.juicenet.carDescription.label = Car Description +channel-type.juicenet.carDescription.description = Car description of vehicle currently or last charged. +channel-type.juicenet.chargingState.label = Charging State +channel-type.juicenet.chargingState.description = The charging state (Start Charging, Smart Charging, Stop Charging). +channel-type.juicenet.chargingState.state.option.start = Start Charging +channel-type.juicenet.chargingState.state.option.smart = Smart Charging +channel-type.juicenet.chargingState.state.option.stop = Stop Charging +channel-type.juicenet.chargingTime.label = Charging Time +channel-type.juicenet.chargingTime.description = Charging time since plug-in time. +channel-type.juicenet.chargingTimeLeft.label = Charging Time Left +channel-type.juicenet.chargingTimeLeft.description = Charging time left. +channel-type.juicenet.current.label = Current +channel-type.juicenet.current.description = Current charging current. +channel-type.juicenet.currentLimit.label = Current Limit +channel-type.juicenet.currentLimit.description = Max charging current allowed. +channel-type.juicenet.ecost.label = Utility Energy Cost +channel-type.juicenet.ecost.description = Cost of electricity from utility company. (currency / kWh) +channel-type.juicenet.energy.label = Current Energy +channel-type.juicenet.energy.description = Current power level of vehicle. +channel-type.juicenet.energyAtPlugin.label = Energy at Plugin +channel-type.juicenet.energyAtPlugin.description = Energy value at the plugging time. +channel-type.juicenet.energyPerMile.label = Energy Hours Per Mile +channel-type.juicenet.energyPerMile.description = Energy Hours Per Mile. +channel-type.juicenet.energyToAdd.label = Energy to Add +channel-type.juicenet.energyToAdd.description = Amount of energy to be added in current session. +channel-type.juicenet.fuelConsumption.label = Fuel consumption +channel-type.juicenet.fuelConsumption.description = Distance per volume (mpg) used in savings calculations. +channel-type.juicenet.gasCost.label = Gas Cost +channel-type.juicenet.gasCost.description = Cost of gasoline used in savings calculations. +channel-type.juicenet.lifetimeEnergy.label = Lifetime Energy +channel-type.juicenet.lifetimeEnergy.description = Total energy delivered to vehicles during lifetime. +channel-type.juicenet.lifetimeSavings.label = Lifetime Savings +channel-type.juicenet.lifetimeSavings.description = EV driving saving during lifetime. +channel-type.juicenet.message.label = State Message +channel-type.juicenet.message.description = This is a message detailing the state of the EV charger. +channel-type.juicenet.name.label = Name +channel-type.juicenet.name.description = Juice Box name. +channel-type.juicenet.override.label = Override State +channel-type.juicenet.override.description = Smart charging is overridden. +channel-type.juicenet.plugUnplugTime.label = Plug/Unplug Time +channel-type.juicenet.plugUnplugTime.description = Last time of either plug-in or plug-out. +channel-type.juicenet.plugUnplugTime.state.pattern = %1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp +channel-type.juicenet.power.label = Charging Power +channel-type.juicenet.power.description = Current charging power. +channel-type.juicenet.savings.label = Savings +channel-type.juicenet.savings.description = Current session EV savings. +channel-type.juicenet.state.label = Device State +channel-type.juicenet.state.description = This is the current device state (Available, Plugged-In, Charging, Error, Disconnected). +channel-type.juicenet.state.state.option.standby = Available +channel-type.juicenet.state.state.option.plugged = Plugged-In +channel-type.juicenet.state.state.option.charging = Charging +channel-type.juicenet.state.state.option.error = Error +channel-type.juicenet.state.state.option.disconnect = Disconnected +channel-type.juicenet.targetTime.label = Target Time +channel-type.juicenet.targetTime.description = “Start charging” start time, or time to start when overriding smart charging. +channel-type.juicenet.targetTime.state.pattern = %1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp +channel-type.juicenet.temperature.label = Temperature +channel-type.juicenet.temperature.description = Current temperature at the unit. +channel-type.juicenet.unitTime.label = Unit Time +channel-type.juicenet.unitTime.description = Current time on the unit. +channel-type.juicenet.unitTime.state.pattern = %1$tB %1$te, %1$tY %1$tl:%1$tM %1$tp +channel-type.juicenet.voltage.label = Voltage +channel-type.juicenet.voltage.description = Current voltage. + +# offline configuration errors + +offline.configuration-error.id-missing = Must include an id in the configuration for the device. +offline.configuration-error.non-existent-device = Device does not exist as part of this JuiceNet Account +offline.configuration-error.bridge-missing = The JuiceBox device must be associated with a JuiceNet Account bridge diff --git a/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-account.xml b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-account.xml new file mode 100644 index 000000000..40e76cf36 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-account.xml @@ -0,0 +1,23 @@ + + + + + + This is the account for which your device(s) are registered at home.juice.net. + + + + + API Token from the user profile page. (https://home.juice.net/Manage) + + + + Interval the device is polled in seconds. + 60 + + + + diff --git a/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-device.xml b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-device.xml new file mode 100644 index 000000000..e12493919 --- /dev/null +++ b/bundles/org.openhab.binding.juicenet/src/main/resources/OH-INF/thing/juicenet-device.xml @@ -0,0 +1,288 @@ + + + + + + + + + + JuiceBox EV Charger + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + unitID + + + + + EV charger Unit ID from the JuiceNet webpage. (https://home.juice.net) + + + + + + String + + Juice Box name. + + + + + String + + The charging state (Start Charging, Smart Charging, Stop Charging). + + + + + + + + recommend + + + + String + + This is the current device state (Available, Plugged-In, Charging, Error, Disconnected). + + + + + + + + + + + + + String + + This is a message detailing the state of the EV charger. + + + + + Switch + + Smart charging is overridden. + + + + + Number:Time + + Charging time left. + Time + + + + + DateTime + + Last time of either plug-in or plug-out. + Time + + + + + DateTime + + “Start charging” start time, or time to start when overriding smart charging. + Time + + + + + DateTime + + Current time on the unit. + Time + + + + + Number:Temperature + + Current temperature at the unit. + Temperature + + + + + Number:ElectricCurrent + + Max charging current allowed. + + + + + Number:ElectricCurrent + + Current charging current. + + + + + Number:ElectricPotential + + Current voltage. + + + + + Number:Energy + + Current power level of vehicle. + + + + + Number + + Current session EV savings. + + + + + Number:Power + + Current charging power. + + + + + Number:Time + + Charging time since plug-in time. + Time + + + + + Number:Energy + + Energy value at the plugging time. + + + + + Number:Energy + + Amount of energy to be added in current session. + + + + + Number:Energy + + Total energy delivered to vehicles during lifetime. + + + + + Number + + EV driving saving during lifetime. + + + + + Number + + Cost of gasoline used in savings calculations. + + + + + Number + + Distance per volume (mpg) used in savings calculations. + + + + + Number + + Cost of electricity from utility company. (currency / kWh) + + + + + Number:Power + + Energy Hours Per Mile. + + + + + String + + Car description of vehicle currently or last charged. + + + + + Number:Energy + + Car battery pack size. + + + + + Number:Length + + Car distance range. + + + + + Number:Power + + Car charging rate. + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 54fdac650..316b6a9ad 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -186,6 +186,7 @@ org.openhab.binding.jablotron org.openhab.binding.jeelink org.openhab.binding.jellyfin + org.openhab.binding.juicenet org.openhab.binding.kaleidescape org.openhab.binding.keba org.openhab.binding.km200