]> git.basschouten.com Git - openhab-addons.git/blob
aadc5a94162afb8b4eaef825bb401cd3f681b19c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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                 case CHANNEL_OVERRIDE: {
259                     if (command instanceof OnOffType onOffCommand && OnOffType.OFF.equals(onOffCommand)) {
260                         api.setOverride(Objects.requireNonNull(token), 0, ((long) 0), 0);
261                     }
262
263                     break;
264                 }
265             }
266         } catch (JuiceNetApiException | InterruptedException e) {
267             handleApiException(e);
268             return;
269         }
270     }
271
272     private void tryQueryDeviceStatusAndInfo() throws JuiceNetApiException, InterruptedException {
273         String apiToken = Objects.requireNonNull(this.token);
274         JuiceNetApi api = getApi();
275         if (api == null) {
276             return;
277         }
278
279         deviceStatus = api.queryDeviceStatus(apiToken);
280
281         if (deviceStatus.infoTimestamp > lastInfoTimestamp) {
282             lastInfoTimestamp = deviceStatus.infoTimestamp;
283
284             deviceInfo = api.queryInfo(apiToken);
285             deviceTouSchedule = api.queryTOUSchedule(apiToken);
286             refreshInfoChannels();
287         }
288
289         int carId = deviceStatus.carId;
290         for (JuiceNetApiCar car : deviceInfo.cars) {
291             if (car.carId == carId) {
292                 this.deviceCar = car;
293                 break;
294             }
295         }
296
297         refreshStatusChannels();
298     }
299
300     public void queryDeviceStatusAndInfo() {
301         logger.trace("queryStatusAndInfo");
302         ThingStatus status = getThing().getStatus();
303
304         if (status != ThingStatus.ONLINE) {
305             goOnline();
306             return;
307         }
308
309         try {
310             tryQueryDeviceStatusAndInfo();
311         } catch (JuiceNetApiException | InterruptedException e) {
312             handleApiException(e);
313             return;
314         }
315     }
316
317     private ZonedDateTime toZonedDateTime(long localEpochSeconds) {
318         return Instant.ofEpochSecond(localEpochSeconds).atZone(timeZoneProvider.getTimeZone());
319     }
320
321     private void refreshStatusChannels() {
322         updateState(CHANNEL_STATE, new StringType(deviceStatus.state));
323
324         if (deviceStatus.targetTime <= deviceStatus.unitTime) {
325             updateState(CHANNEL_CHARGING_STATE, new StringType("start"));
326         } else if ((deviceStatus.targetTime - deviceStatus.unitTime) < TimeUnit.DAYS.toSeconds(2)) {
327             updateState(CHANNEL_CHARGING_STATE, new StringType("smart"));
328         } else {
329             updateState(CHANNEL_CHARGING_STATE, new StringType("stop"));
330         }
331
332         updateState(CHANNEL_MESSAGE, new StringType(deviceStatus.message));
333         updateState(CHANNEL_OVERRIDE, OnOffType.from(deviceStatus.showOverride));
334         updateState(CHANNEL_CHARGING_TIME_LEFT, new QuantityType<>(deviceStatus.chargingTimeLeft, Units.SECOND));
335         updateState(CHANNEL_PLUG_UNPLUG_TIME, new DateTimeType(toZonedDateTime(deviceStatus.plugUnplugTime)));
336         updateState(CHANNEL_TARGET_TIME, new DateTimeType(toZonedDateTime(deviceStatus.targetTime)));
337         updateState(CHANNEL_UNIT_TIME, new DateTimeType(toZonedDateTime(deviceStatus.utcTime)));
338         updateState(CHANNEL_TEMPERATURE, new QuantityType<>(deviceStatus.temperature, SIUnits.CELSIUS));
339         updateState(CHANNEL_CURRENT_LIMIT, new QuantityType<>(deviceStatus.charging.ampsLimit, Units.AMPERE));
340         updateState(CHANNEL_CURRENT, new QuantityType<>(deviceStatus.charging.ampsCurrent, Units.AMPERE));
341         updateState(CHANNEL_VOLTAGE, new QuantityType<>(deviceStatus.charging.voltage, Units.VOLT));
342         updateState(CHANNEL_ENERGY, new QuantityType<>(deviceStatus.charging.whEnergy, Units.WATT_HOUR));
343         updateState(CHANNEL_SAVINGS, new DecimalType(deviceStatus.charging.savings / 100.0));
344         updateState(CHANNEL_POWER, new QuantityType<>(deviceStatus.charging.wattPower, Units.WATT));
345         updateState(CHANNEL_CHARGING_TIME, new QuantityType<>(deviceStatus.charging.secondsCharging, Units.SECOND));
346         updateState(CHANNEL_ENERGY_AT_PLUGIN,
347                 new QuantityType<>(deviceStatus.charging.whEnergyAtPlugin, Units.WATT_HOUR));
348         updateState(CHANNEL_ENERGY_TO_ADD, new QuantityType<>(deviceStatus.charging.whEnergyToAdd, Units.WATT_HOUR));
349         updateState(CHANNEL_LIFETIME_ENERGY, new QuantityType<>(deviceStatus.lifetime.whEnergy, Units.WATT_HOUR));
350         updateState(CHANNEL_LIFETIME_SAVINGS, new DecimalType(deviceStatus.lifetime.savings / 100.0));
351
352         // update Car items
353         updateState(CHANNEL_CAR_DESCRIPTION, new StringType(deviceCar.description));
354         updateState(CHANNEL_CAR_BATTERY_SIZE, new QuantityType<>(deviceCar.batterySizeWH, Units.WATT_HOUR));
355         updateState(CHANNEL_CAR_BATTERY_RANGE, new QuantityType<>(deviceCar.batteryRangeM, ImperialUnits.MILE));
356         updateState(CHANNEL_CAR_CHARGING_RATE, new QuantityType<>(deviceCar.chargingRateW, Units.WATT));
357     }
358
359     private void refreshInfoChannels() {
360         updateState(CHANNEL_NAME, new StringType(name));
361         updateState(CHANNEL_GAS_COST, new DecimalType(deviceInfo.gasCost / 100.0));
362         // currently there is no unit defined for fuel consumption
363         updateState(CHANNEL_FUEL_CONSUMPTION, new DecimalType(deviceInfo.mpg));
364         updateState(CHANNEL_ECOST, new DecimalType(deviceInfo.ecost / 100.0));
365         updateState(CHANNEL_ENERGY_PER_MILE, new DecimalType(deviceInfo.whPerMile));
366     }
367 }