[juicenet] Initial contribution (#10768)

Signed-off-by: Jeff James <jeff@james-online.com>
This commit is contained in:
jsjames
2022-11-13 03:27:43 -08:00
committed by GitHub
parent fbd06ec709
commit 71d1226505
29 changed files with 2117 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.juicenet-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-juicenet" description="JuiceNet Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.juicenet/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link JuiceNetBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetBindingConstants {
private static final String BINDING_ID = "juicenet";
// List of Bridge Type
public static final String BRIDGE = "account";
// List of all Device Types
public static final String DEVICE = "device";
// List of all Bridge Thing Type UIDs
public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, BRIDGE);
// List of all Thing Type UIDs
public static final ThingTypeUID DEVICE_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE);
// Device config parameter
public static final String PARAMETER_UNIT_ID = "unitID";
// Device properties
public static final String PROPERTY_NAME = "name";
// List of all Channel ids
public static final String CHANNEL_NAME = "name";
public static final String CHANNEL_CHARGING_STATE = "chargingState";
public static final String CHANNEL_STATE = "state";
public static final String CHANNEL_MESSAGE = "message";
public static final String CHANNEL_OVERRIDE = "override";
public static final String CHANNEL_CHARGING_TIME_LEFT = "chargingTimeLeft";
public static final String CHANNEL_PLUG_UNPLUG_TIME = "plugUnplugTime";
public static final String CHANNEL_TARGET_TIME = "targetTime";
public static final String CHANNEL_UNIT_TIME = "unitTime";
public static final String CHANNEL_TEMPERATURE = "temperature";
public static final String CHANNEL_CURRENT_LIMIT = "currentLimit";
public static final String CHANNEL_CURRENT = "current";
public static final String CHANNEL_VOLTAGE = "voltage";
public static final String CHANNEL_ENERGY = "energy";
public static final String CHANNEL_SAVINGS = "savings";
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_CHARGING_TIME = "chargingTime";
public static final String CHANNEL_ENERGY_AT_PLUGIN = "energyAtPlugin";
public static final String CHANNEL_ENERGY_TO_ADD = "energyToAdd";
public static final String CHANNEL_LIFETIME_ENERGY = "lifetimeEnergy";
public static final String CHANNEL_LIFETIME_SAVINGS = "lifetimeSavings";
public static final String CHANNEL_GAS_COST = "gasCost";
public static final String CHANNEL_FUEL_CONSUMPTION = "fuelConsumption";
public static final String CHANNEL_ECOST = "ecost";
public static final String CHANNEL_ENERGY_PER_MILE = "energyPerMile";
public static final String CHANNEL_CAR_DESCRIPTION = "carDescription";
public static final String CHANNEL_CAR_BATTERY_SIZE = "carBatterySize";
public static final String CHANNEL_CAR_BATTERY_RANGE = "carBatteryRange";
public static final String CHANNEL_CAR_CHARGING_RATE = "carChargingRate";
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal;
import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.juicenet.internal.handler.JuiceNetBridgeHandler;
import org.openhab.binding.juicenet.internal.handler.JuiceNetDeviceHandler;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link JuiceNetHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.juicenet", service = ThingHandlerFactory.class)
public class JuiceNetHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(BRIDGE_THING_TYPE, DEVICE_THING_TYPE);
private final HttpClientFactory httpClientFactory;
private final TimeZoneProvider timeZoneProvider;
@Activate
public JuiceNetHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference TimeZoneProvider timeZoneProvider) {
this.httpClientFactory = httpClientFactory;
this.timeZoneProvider = timeZoneProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(BRIDGE_THING_TYPE)) {
return new JuiceNetBridgeHandler((Bridge) thing, httpClientFactory.getCommonHttpClient());
} else if (thingTypeUID.equals(DEVICE_THING_TYPE)) {
return new JuiceNetDeviceHandler(thing, timeZoneProvider);
}
return null;
}
}

View File

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

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link JuiceNetApiException} implements an API Exception
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiException extends Exception {
private static final long serialVersionUID = 5421236828224242152L;
public JuiceNetApiException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiCar } implements DTO for Car API call
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiCar {
@SerializedName("car_id")
public int carId;
public String description = "";
@SerializedName("battery_size_wh")
public int batterySizeWH;
@SerializedName("battery_range_m")
public int batteryRangeM;
@SerializedName("charging_rate_w")
public int chargingRateW;
@SerializedName("model_id")
public String modelId = "";
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiDevice } implements DTO for Device Info API call
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiDevice {
public String name = "";
public String token = "";
@SerializedName("unit_id")
public String unitId = "";
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetDeviceChargingStatus } implements DTO for device charging status
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiDeviceChargingStatus {
@SerializedName("amps_limit")
public int ampsLimit;
@SerializedName("amps_current")
public float ampsCurrent;
public int voltage;
@SerializedName("wh_energy")
public int whEnergy;
public int savings;
@SerializedName("watt_power")
public int wattPower;
@SerializedName("seconds_charging")
public int secondsCharging;
@SerializedName("wh_energy_at_plugin")
public int whEnergyAtPlugin;
@SerializedName("wh_energy_to_add")
public int whEnergyToAdd;
public int flags;
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiDeviceLifetimeStatus } implements DTO for Device Lifetime Status
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiDeviceLifetimeStatus {
@SerializedName("wh_energy")
public int whEnergy;
public int savings;
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiDeviceStatus } implements DTO for Device Status
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiDeviceStatus {
@SerializedName("ID")
public String id = "";
@SerializedName("info_timestamp")
public Long infoTimestamp = (long) 0;
@SerializedName("show_override")
public boolean showOverride;
public String state = "";
public JuiceNetApiDeviceChargingStatus charging = new JuiceNetApiDeviceChargingStatus();
public JuiceNetApiDeviceLifetimeStatus lifetime = new JuiceNetApiDeviceLifetimeStatus();
@SerializedName("charging_time_left")
public int chargingTimeLeft;
@SerializedName("plug_unplug_time")
public Long plugUnplugTime = (long) 0;
@SerializedName("target_time")
public Long targetTime = (long) 0;
@SerializedName("unit_time")
public Long unitTime = (long) 0;
@SerializedName("utc_time")
public Long utcTime = (long) 0;
@SerializedName("default_target_time")
public long defaultTargetTime = 0;
@SerializedName("car_id")
public int carId;
public int temperature;
public String message = "";
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiInfo } implements DTO for Info
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiInfo {
public String name = "";
public String address = "";
public String city = "";
public String zip = "";
@SerializedName("country_code")
public String countryCode = "";
public String ip = "";
@SerializedName("gascost")
public int gasCost;
public int mpg;
public int ecost;
@SerializedName("whpermile")
public int whPerMile;
public String timeZoneId = "";
@SerializedName("amps_wire_rating")
public int ampsWireRating;
@SerializedName("amps_unit_rating")
public int ampsUnitRating;
public JuiceNetApiCar[] cars = {};
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* {@link JuiceNetApiTouDay } implements DTO for TOU settings
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiTouDay {
public int start;
public int end;
@SerializedName("car_ready_by")
public int carReadyBy;
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link JuiceNetApiTouSchedule } implements DTO for TOU schedule
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetApiTouSchedule {
public String type = "";
public JuiceNetApiTouDay weekday = new JuiceNetApiTouDay();
public JuiceNetApiTouDay weenend = new JuiceNetApiTouDay();
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link JuiceNetBridgeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetBridgeConfiguration {
public String apiToken = "";
public int refreshInterval = 60;
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.juicenet.internal.discovery;
import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.juicenet.internal.handler.JuiceNetBridgeHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link JuiceNetDiscoveryService} discovers all devices/zones reported by the FlumeTech Cloud. This requires the
* api
* key to get access to the cloud data.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class JuiceNetDiscoveryService extends AbstractDiscoveryService
implements DiscoveryService, ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(JuiceNetDiscoveryService.class);
private @Nullable JuiceNetBridgeHandler bridgeHandler;
private static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Set.of(DEVICE_THING_TYPE);
public JuiceNetDiscoveryService() {
super(DISCOVERABLE_THING_TYPES_UIDS, 0, false);
}
@Override
public void activate() {
super.activate(null);
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
protected synchronized void startScan() {
Objects.requireNonNull(bridgeHandler).iterateApiDevices();
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof JuiceNetBridgeHandler) {
JuiceNetBridgeHandler bridgeHandler = (JuiceNetBridgeHandler) handler;
bridgeHandler.setDiscoveryService(this);
this.bridgeHandler = bridgeHandler;
} else {
this.bridgeHandler = null;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.bridgeHandler;
}
public void notifyDiscoveryDevice(String id, String name) {
JuiceNetBridgeHandler bridgeHandler = this.bridgeHandler;
Objects.requireNonNull(bridgeHandler, "Discovery with null bridgehandler.");
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
ThingUID uid = new ThingUID(DEVICE_THING_TYPE, bridgeUID, id);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
.withProperty(PARAMETER_UNIT_ID, id).withLabel(name).build();
thingDiscovered(result);
logger.debug("Discovered JuiceNetDevice {} - {}", uid, name);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="juicenet"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="account">
<label>JuiceNet Account</label>
<description>This is the account for which your device(s) are registered at home.juice.net.</description>
<config-description>
<parameter name="apiToken" type="text" required="true">
<label>API Token</label>
<description>API Token from the user profile page. (https://home.juice.net/Manage) </description>
</parameter>
<parameter name="refreshInterval" type="integer" unit="s" min="60">
<label>Refresh Interval</label>
<description>Interval the device is polled in seconds.</description>
<default>60</default>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

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