]> git.basschouten.com Git - openhab-addons.git/blob
333f832aad4e506ec8c647e2e55f64fb20f9df7d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.juicenet.internal.handler;
14
15 import static org.openhab.binding.juicenet.internal.JuiceNetBindingConstants.*;
16
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;
22
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;
52
53 /**
54  * The {@link JuiceNetDeviceHandler} is responsible for handling commands, which are
55  * sent to one of the channels.
56  *
57  * @author Jeff James - Initial contribution
58  */
59 @NonNullByDefault
60 public class JuiceNetDeviceHandler extends BaseThingHandler {
61
62     private final Logger logger = LoggerFactory.getLogger(JuiceNetDeviceHandler.class);
63
64     private final TimeZoneProvider timeZoneProvider;
65
66     // properties
67     private String name = "";
68
69     private String token = "";
70     private long targetTimeTou = 0;
71     private long lastInfoTimestamp = 0;
72
73     JuiceNetApiDeviceStatus deviceStatus = new JuiceNetApiDeviceStatus();
74     JuiceNetApiInfo deviceInfo = new JuiceNetApiInfo();
75     JuiceNetApiTouSchedule deviceTouSchedule = new JuiceNetApiTouSchedule();
76     JuiceNetApiCar deviceCar = new JuiceNetApiCar();
77
78     public JuiceNetDeviceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
79         super(thing);
80
81         this.timeZoneProvider = timeZoneProvider;
82     }
83
84     public void setNameAndToken(String name, String token) {
85         logger.trace("setNameAndToken");
86         this.token = token;
87
88         if (!name.equals(this.name)) {
89             updateProperty(PROPERTY_NAME, name);
90             this.name = name;
91         }
92
93         if (getThing().getStatus() != ThingStatus.ONLINE) {
94             goOnline();
95         }
96     }
97
98     @Override
99     public void initialize() {
100         logger.trace("Device initialized: {}", Objects.requireNonNull(getThing().getUID()));
101         Configuration configuration = getThing().getConfiguration();
102
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");
107             return;
108         }
109
110         updateStatus(ThingStatus.UNKNOWN);
111
112         // This device will go ONLINE on the first successful API call in queryDeviceStatusAndInfo
113     }
114
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();
121         } else {
122             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.toString());
123         }
124     }
125
126     private void goOnline() {
127         logger.trace("goOnline");
128         if (this.getThing().getStatus() == ThingStatus.ONLINE) {
129             return;
130         }
131
132         if (token.isEmpty()) {
133             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
134                     "@text/offline.configuration-error.non-existent-device");
135             return;
136         }
137
138         try {
139             tryQueryDeviceStatusAndInfo();
140         } catch (JuiceNetApiException | InterruptedException e) {
141             handleApiException(e);
142             return;
143         }
144
145         updateStatus(ThingStatus.ONLINE);
146     }
147
148     @Nullable
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");
154             return null;
155         }
156         BridgeHandler handler = Objects.requireNonNull(bridge.getHandler());
157
158         return ((JuiceNetBridgeHandler) handler).getApi();
159     }
160
161     @Override
162     public void handleCommand(ChannelUID channelUID, Command command) {
163         JuiceNetApi api = getApi();
164         if (api == null) {
165             return;
166         }
167
168         if (command instanceof RefreshType) {
169             switch (channelUID.getId()) {
170                 case CHANNEL_NAME:
171                 case CHANNEL_STATE:
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:
182                 case CHANNEL_ENERGY:
183                 case CHANNEL_SAVINGS:
184                 case CHANNEL_POWER:
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();
195                     break;
196                 case CHANNEL_GAS_COST:
197                 case CHANNEL_FUEL_CONSUMPTION:
198                 case CHANNEL_ECOST:
199                 case CHANNEL_ENERGY_PER_MILE:
200                     refreshInfoChannels();
201                     break;
202             }
203
204             return;
205         }
206
207         try {
208             switch (channelUID.getId()) {
209                 case CHANNEL_CURRENT_LIMIT:
210                     int limit = ((QuantityType<?>) command).intValue();
211                     api.setCurrentLimit(Objects.requireNonNull(token), limit);
212                     break;
213                 case CHANNEL_TARGET_TIME: {
214                     int energyAtPlugin = 0;
215                     int energyToAdd = deviceCar.batterySizeWH;
216
217                     if (!(command instanceof DateTimeType)) {
218                         logger.info("Target Time is not an instance of DateTimeType");
219                         return;
220                     }
221
222                     ZonedDateTime datetime = ((DateTimeType) command).getZonedDateTime();
223                     Long targetTime = datetime.toEpochSecond() + datetime.get(ChronoField.OFFSET_SECONDS);
224                     logger.debug("DateTime: {} - {}", datetime.toString(), targetTime);
225
226                     api.setOverride(Objects.requireNonNull(token), energyAtPlugin, targetTime, energyToAdd);
227
228                     break;
229                 }
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;
235
236                     switch (state) {
237                         case "stop":
238                             if (targetTimeTou == 0) {
239                                 targetTimeTou = deviceStatus.targetTime;
240                             }
241                             overrideTime = deviceStatus.unitTime + 31556926;
242                             break;
243                         case "start":
244                             if (targetTimeTou == 0) {
245                                 targetTimeTou = deviceStatus.targetTime;
246                             }
247                             overrideTime = deviceStatus.unitTime;
248                             break;
249                         case "smart":
250                             overrideTime = deviceStatus.defaultTargetTime;
251                             break;
252                     }
253
254                     api.setOverride(Objects.requireNonNull(token), energyAtPlugin, overrideTime, energyToAdd);
255
256                     break;
257                 }
258             }
259         } catch (JuiceNetApiException | InterruptedException e) {
260             handleApiException(e);
261             return;
262         }
263     }
264
265     private void tryQueryDeviceStatusAndInfo() throws JuiceNetApiException, InterruptedException {
266         String apiToken = Objects.requireNonNull(this.token);
267         JuiceNetApi api = getApi();
268         if (api == null) {
269             return;
270         }
271
272         deviceStatus = api.queryDeviceStatus(apiToken);
273
274         if (deviceStatus.infoTimestamp > lastInfoTimestamp) {
275             lastInfoTimestamp = deviceStatus.infoTimestamp;
276
277             deviceInfo = api.queryInfo(apiToken);
278             deviceTouSchedule = api.queryTOUSchedule(apiToken);
279             refreshInfoChannels();
280         }
281
282         int carId = deviceStatus.carId;
283         for (JuiceNetApiCar car : deviceInfo.cars) {
284             if (car.carId == carId) {
285                 this.deviceCar = car;
286                 break;
287             }
288         }
289
290         refreshStatusChannels();
291     }
292
293     public void queryDeviceStatusAndInfo() {
294         logger.trace("queryStatusAndInfo");
295         ThingStatus status = getThing().getStatus();
296
297         if (status != ThingStatus.ONLINE) {
298             goOnline();
299             return;
300         }
301
302         try {
303             tryQueryDeviceStatusAndInfo();
304         } catch (JuiceNetApiException | InterruptedException e) {
305             handleApiException(e);
306             return;
307         }
308     }
309
310     private ZonedDateTime toZonedDateTime(long localEpochSeconds) {
311         return Instant.ofEpochSecond(localEpochSeconds).atZone(timeZoneProvider.getTimeZone());
312     }
313
314     private void refreshStatusChannels() {
315         updateState(CHANNEL_STATE, new StringType(deviceStatus.state));
316
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"));
321         } else {
322             updateState(CHANNEL_CHARGING_STATE, new StringType("stop"));
323         }
324
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));
344
345         // update Car items
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));
350     }
351
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));
359     }
360 }