2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.juicenet.internal.handler;
15 import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*;
17 import java.time.Instant;
18 import java.time.ZonedDateTime;
19 import java.time.temporal.ChronoField;
20 import java.util.Objects;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.juicenet.internal.api.JuiceNetApi;
26 import org.openhab.binding.juicenet.internal.api.JuiceNetApiException;
27 import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiCar;
28 import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiDeviceStatus;
29 import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiInfo;
30 import org.openhab.binding.juicenet.internal.api.dto.JuiceNetApiTouSchedule;
31 import org.openhab.core.config.core.Configuration;
32 import org.openhab.core.i18n.TimeZoneProvider;
33 import org.openhab.core.library.types.DateTimeType;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.QuantityType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.library.unit.ImperialUnits;
39 import org.openhab.core.library.unit.SIUnits;
40 import org.openhab.core.library.unit.Units;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.thing.binding.BridgeHandler;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * The {@link JuiceNetDeviceHandler} is responsible for handling commands, which are
55 * sent to one of the channels.
57 * @author Jeff James - Initial contribution
60 public class JuiceNetDeviceHandler extends BaseThingHandler {
62 private final Logger logger = LoggerFactory.getLogger(JuiceNetDeviceHandler.class);
64 private final TimeZoneProvider timeZoneProvider;
67 private String name = "";
69 private String token = "";
70 private long targetTimeTou = 0;
71 private long lastInfoTimestamp = 0;
73 JuiceNetApiDeviceStatus deviceStatus = new JuiceNetApiDeviceStatus();
74 JuiceNetApiInfo deviceInfo = new JuiceNetApiInfo();
75 JuiceNetApiTouSchedule deviceTouSchedule = new JuiceNetApiTouSchedule();
76 JuiceNetApiCar deviceCar = new JuiceNetApiCar();
78 public JuiceNetDeviceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
81 this.timeZoneProvider = timeZoneProvider;
84 public void setNameAndToken(String name, String token) {
85 logger.trace("setNameAndToken");
88 if (!name.equals(this.name)) {
89 updateProperty(PROPERTY_NAME, name);
93 if (getThing().getStatus() != ThingStatus.ONLINE) {
99 public void initialize() {
100 logger.trace("Device initialized: {}", Objects.requireNonNull(getThing().getUID()));
101 Configuration configuration = getThing().getConfiguration();
103 String stringId = configuration.get(PARAMETER_UNIT_ID).toString();
104 if (stringId.isEmpty()) {
105 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
106 "@text/offline.configuration-error.id-missing");
110 updateStatus(ThingStatus.UNKNOWN);
112 // This device will go ONLINE on the first successful API call in queryDeviceStatusAndInfo
115 private void handleApiException(Exception e) {
116 if (e instanceof JuiceNetApiException) {
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.toString());
118 } else if (e instanceof InterruptedException) {
119 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.toString());
120 Thread.currentThread().interrupt();
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.toString());
126 private void goOnline() {
127 logger.trace("goOnline");
128 if (this.getThing().getStatus() == ThingStatus.ONLINE) {
132 if (token.isEmpty()) {
133 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
134 "@text/offline.configuration-error.non-existent-device");
139 tryQueryDeviceStatusAndInfo();
140 } catch (JuiceNetApiException | InterruptedException e) {
141 handleApiException(e);
145 updateStatus(ThingStatus.ONLINE);
149 private JuiceNetApi getApi() {
150 Bridge bridge = getBridge();
151 if (bridge == null) {
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
153 "@text/offline.configuration-error.bridge-missing");
156 BridgeHandler handler = Objects.requireNonNull(bridge.getHandler());
158 return ((JuiceNetBridgeHandler) handler).getApi();
162 public void handleCommand(ChannelUID channelUID, Command command) {
163 JuiceNetApi api = getApi();
168 if (command instanceof RefreshType) {
169 switch (channelUID.getId()) {
172 case CHANNEL_MESSAGE:
173 case CHANNEL_OVERRIDE:
174 case CHANNEL_CHARGING_TIME_LEFT:
175 case CHANNEL_PLUG_UNPLUG_TIME:
176 case CHANNEL_TARGET_TIME:
177 case CHANNEL_UNIT_TIME:
178 case CHANNEL_TEMPERATURE:
179 case CHANNEL_CURRENT_LIMIT:
180 case CHANNEL_CURRENT:
181 case CHANNEL_VOLTAGE:
183 case CHANNEL_SAVINGS:
185 case CHANNEL_CHARGING_TIME:
186 case CHANNEL_ENERGY_AT_PLUGIN:
187 case CHANNEL_ENERGY_TO_ADD:
188 case CHANNEL_LIFETIME_ENERGY:
189 case CHANNEL_LIFETIME_SAVINGS:
190 case CHANNEL_CAR_DESCRIPTION:
191 case CHANNEL_CAR_BATTERY_SIZE:
192 case CHANNEL_CAR_BATTERY_RANGE:
193 case CHANNEL_CAR_CHARGING_RATE:
194 refreshStatusChannels();
196 case CHANNEL_GAS_COST:
197 case CHANNEL_FUEL_CONSUMPTION:
199 case CHANNEL_ENERGY_PER_MILE:
200 refreshInfoChannels();
208 switch (channelUID.getId()) {
209 case CHANNEL_CURRENT_LIMIT:
210 int limit = ((QuantityType<?>) command).intValue();
211 api.setCurrentLimit(Objects.requireNonNull(token), limit);
213 case CHANNEL_TARGET_TIME: {
214 int energyAtPlugin = 0;
215 int energyToAdd = deviceCar.batterySizeWH;
217 if (!(command instanceof DateTimeType)) {
218 logger.info("Target Time is not an instance of DateTimeType");
222 ZonedDateTime datetime = ((DateTimeType) command).getZonedDateTime();
223 Long targetTime = datetime.toEpochSecond() + datetime.get(ChronoField.OFFSET_SECONDS);
224 logger.debug("DateTime: {} - {}", datetime.toString(), targetTime);
226 api.setOverride(Objects.requireNonNull(token), energyAtPlugin, targetTime, energyToAdd);
230 case CHANNEL_CHARGING_STATE: {
231 String state = ((StringType) command).toString();
232 Long overrideTime = deviceStatus.unitTime;
233 int energyAtPlugin = 0;
234 int energyToAdd = deviceCar.batterySizeWH;
238 if (targetTimeTou == 0) {
239 targetTimeTou = deviceStatus.targetTime;
241 overrideTime = deviceStatus.unitTime + 31556926;
244 if (targetTimeTou == 0) {
245 targetTimeTou = deviceStatus.targetTime;
247 overrideTime = deviceStatus.unitTime;
250 overrideTime = deviceStatus.defaultTargetTime;
254 api.setOverride(Objects.requireNonNull(token), energyAtPlugin, overrideTime, energyToAdd);
259 } catch (JuiceNetApiException | InterruptedException e) {
260 handleApiException(e);
265 private void tryQueryDeviceStatusAndInfo() throws JuiceNetApiException, InterruptedException {
266 String apiToken = Objects.requireNonNull(this.token);
267 JuiceNetApi api = getApi();
272 deviceStatus = api.queryDeviceStatus(apiToken);
274 if (deviceStatus.infoTimestamp > lastInfoTimestamp) {
275 lastInfoTimestamp = deviceStatus.infoTimestamp;
277 deviceInfo = api.queryInfo(apiToken);
278 deviceTouSchedule = api.queryTOUSchedule(apiToken);
279 refreshInfoChannels();
282 int carId = deviceStatus.carId;
283 for (JuiceNetApiCar car : deviceInfo.cars) {
284 if (car.carId == carId) {
285 this.deviceCar = car;
290 refreshStatusChannels();
293 public void queryDeviceStatusAndInfo() {
294 logger.trace("queryStatusAndInfo");
295 ThingStatus status = getThing().getStatus();
297 if (status != ThingStatus.ONLINE) {
303 tryQueryDeviceStatusAndInfo();
304 } catch (JuiceNetApiException | InterruptedException e) {
305 handleApiException(e);
310 private ZonedDateTime toZonedDateTime(long localEpochSeconds) {
311 return Instant.ofEpochSecond(localEpochSeconds).atZone(timeZoneProvider.getTimeZone());
314 private void refreshStatusChannels() {
315 updateState(CHANNEL_STATE, new StringType(deviceStatus.state));
317 if (deviceStatus.targetTime <= deviceStatus.unitTime) {
318 updateState(CHANNEL_CHARGING_STATE, new StringType("start"));
319 } else if ((deviceStatus.targetTime - deviceStatus.unitTime) < TimeUnit.DAYS.toSeconds(2)) {
320 updateState(CHANNEL_CHARGING_STATE, new StringType("smart"));
322 updateState(CHANNEL_CHARGING_STATE, new StringType("stop"));
325 updateState(CHANNEL_MESSAGE, new StringType(deviceStatus.message));
326 updateState(CHANNEL_OVERRIDE, OnOffType.from(deviceStatus.showOverride));
327 updateState(CHANNEL_CHARGING_TIME_LEFT, new QuantityType<>(deviceStatus.chargingTimeLeft, Units.SECOND));
328 updateState(CHANNEL_PLUG_UNPLUG_TIME, new DateTimeType(toZonedDateTime(deviceStatus.plugUnplugTime)));
329 updateState(CHANNEL_TARGET_TIME, new DateTimeType(toZonedDateTime(deviceStatus.targetTime)));
330 updateState(CHANNEL_UNIT_TIME, new DateTimeType(toZonedDateTime(deviceStatus.utcTime)));
331 updateState(CHANNEL_TEMPERATURE, new QuantityType<>(deviceStatus.temperature, SIUnits.CELSIUS));
332 updateState(CHANNEL_CURRENT_LIMIT, new QuantityType<>(deviceStatus.charging.ampsLimit, Units.AMPERE));
333 updateState(CHANNEL_CURRENT, new QuantityType<>(deviceStatus.charging.ampsCurrent, Units.AMPERE));
334 updateState(CHANNEL_VOLTAGE, new QuantityType<>(deviceStatus.charging.voltage, Units.VOLT));
335 updateState(CHANNEL_ENERGY, new QuantityType<>(deviceStatus.charging.whEnergy, Units.WATT_HOUR));
336 updateState(CHANNEL_SAVINGS, new DecimalType(deviceStatus.charging.savings / 100.0));
337 updateState(CHANNEL_POWER, new QuantityType<>(deviceStatus.charging.wattPower, Units.WATT));
338 updateState(CHANNEL_CHARGING_TIME, new QuantityType<>(deviceStatus.charging.secondsCharging, Units.SECOND));
339 updateState(CHANNEL_ENERGY_AT_PLUGIN,
340 new QuantityType<>(deviceStatus.charging.whEnergyAtPlugin, Units.WATT_HOUR));
341 updateState(CHANNEL_ENERGY_TO_ADD, new QuantityType<>(deviceStatus.charging.whEnergyToAdd, Units.WATT_HOUR));
342 updateState(CHANNEL_LIFETIME_ENERGY, new QuantityType<>(deviceStatus.lifetime.whEnergy, Units.WATT_HOUR));
343 updateState(CHANNEL_LIFETIME_SAVINGS, new DecimalType(deviceStatus.lifetime.savings / 100.0));
346 updateState(CHANNEL_CAR_DESCRIPTION, new StringType(deviceCar.description));
347 updateState(CHANNEL_CAR_BATTERY_SIZE, new QuantityType<>(deviceCar.batterySizeWH, Units.WATT_HOUR));
348 updateState(CHANNEL_CAR_BATTERY_RANGE, new QuantityType<>(deviceCar.batteryRangeM, ImperialUnits.MILE));
349 updateState(CHANNEL_CAR_CHARGING_RATE, new QuantityType<>(deviceCar.chargingRateW, Units.WATT));
352 private void refreshInfoChannels() {
353 updateState(CHANNEL_NAME, new StringType(name));
354 updateState(CHANNEL_GAS_COST, new DecimalType(deviceInfo.gasCost / 100.0));
355 // currently there is no unit defined for fuel consumption
356 updateState(CHANNEL_FUEL_CONSUMPTION, new DecimalType(deviceInfo.mpg));
357 updateState(CHANNEL_ECOST, new DecimalType(deviceInfo.ecost / 100.0));
358 updateState(CHANNEL_ENERGY_PER_MILE, new DecimalType(deviceInfo.whPerMile));