]> git.basschouten.com Git - openhab-addons.git/blob
dd8a369d3dcbd93495d0862ec0655c3e39e0b74c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.airvisualnode.internal.handler;
14
15 import static org.openhab.binding.airvisualnode.internal.AirVisualNodeBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.MICRO;
17 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
18 import static org.openhab.core.library.unit.SIUnits.CUBIC_METRE;
19 import static org.openhab.core.library.unit.SIUnits.GRAM;
20 import static org.openhab.core.library.unit.SmartHomeUnits.ONE;
21 import static org.openhab.core.library.unit.SmartHomeUnits.PARTS_PER_MILLION;
22 import static org.openhab.core.library.unit.SmartHomeUnits.PERCENT;
23
24 import java.io.IOException;
25 import java.math.BigDecimal;
26 import java.nio.charset.StandardCharsets;
27 import java.time.Duration;
28 import java.time.Instant;
29 import java.time.ZoneId;
30 import java.time.ZonedDateTime;
31 import java.time.zone.ZoneRules;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
34
35 import org.apache.commons.io.IOUtils;
36 import org.openhab.binding.airvisualnode.internal.config.AirVisualNodeConfig;
37 import org.openhab.binding.airvisualnode.internal.json.NodeData;
38 import org.openhab.core.library.types.DateTimeType;
39 import org.openhab.core.library.types.DecimalType;
40 import org.openhab.core.library.types.QuantityType;
41 import org.openhab.core.thing.Channel;
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.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.State;
50 import org.openhab.core.types.UnDefType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import com.google.gson.FieldNamingPolicy;
55 import com.google.gson.Gson;
56 import com.google.gson.GsonBuilder;
57
58 import jcifs.smb.NtlmPasswordAuthentication;
59 import jcifs.smb.SmbFile;
60 import jcifs.smb.SmbFileInputStream;
61
62 /**
63  * The {@link AirVisualNodeHandler} is responsible for handling commands, which are
64  * sent to one of the channels.
65  *
66  * @author Victor Antonovich - Initial contribution
67  */
68 public class AirVisualNodeHandler extends BaseThingHandler {
69
70     private final Logger logger = LoggerFactory.getLogger(AirVisualNodeHandler.class);
71
72     public static final String NODE_JSON_FILE = "latest_config_measurements.json";
73
74     private final Gson gson;
75
76     private ScheduledFuture<?> pollFuture;
77
78     private long refreshInterval;
79
80     private String nodeAddress;
81
82     private String nodeUsername;
83
84     private String nodePassword;
85
86     private String nodeShareName;
87
88     private NodeData nodeData;
89
90     public AirVisualNodeHandler(Thing thing) {
91         super(thing);
92         gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
93     }
94
95     @Override
96     public void initialize() {
97         logger.debug("Initializing AirVisual Node handler");
98
99         AirVisualNodeConfig config = getConfigAs(AirVisualNodeConfig.class);
100
101         if (config.address == null) {
102             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Node address must be set");
103             return;
104         }
105         this.nodeAddress = config.address;
106
107         this.nodeUsername = config.username;
108
109         if (config.password == null) {
110             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Node password must be set");
111             return;
112         }
113         this.nodePassword = config.password;
114
115         this.nodeShareName = config.share;
116
117         this.refreshInterval = config.refresh * 1000L;
118
119         schedulePoll();
120     }
121
122     @Override
123     public void handleCommand(ChannelUID channelUID, Command command) {
124         if (command instanceof RefreshType) {
125             updateChannel(channelUID.getId(), true);
126         } else {
127             logger.debug("Can not handle command '{}'", command);
128         }
129     }
130
131     @Override
132     public void handleRemoval() {
133         super.handleRemoval();
134         stopPoll();
135     }
136
137     @Override
138     public void dispose() {
139         super.dispose();
140         stopPoll();
141     }
142
143     private synchronized void stopPoll() {
144         if (pollFuture != null && !pollFuture.isCancelled()) {
145             pollFuture.cancel(false);
146         }
147     }
148
149     private synchronized void schedulePoll() {
150         logger.debug("Scheduling poll for 500ms out, then every {} ms", refreshInterval);
151         pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 500, refreshInterval, TimeUnit.MILLISECONDS);
152     }
153
154     private void poll() {
155         try {
156             logger.debug("Polling for state");
157             pollNode();
158             updateStatus(ThingStatus.ONLINE);
159         } catch (IOException e) {
160             logger.debug("Could not connect to Node", e);
161             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
162         }
163     }
164
165     private void pollNode() throws IOException {
166         String jsonData = getNodeJsonData();
167         NodeData currentNodeData = gson.fromJson(jsonData, NodeData.class);
168         if (nodeData == null || currentNodeData.getStatus().getDatetime() > nodeData.getStatus().getDatetime()) {
169             nodeData = currentNodeData;
170             // Update all channels from the updated Node data
171             for (Channel channel : getThing().getChannels()) {
172                 updateChannel(channel.getUID().getId(), false);
173             }
174         }
175     }
176
177     private String getNodeJsonData() throws IOException {
178         String url = "smb://" + nodeAddress + "/" + nodeShareName + "/" + NODE_JSON_FILE;
179         NtlmPasswordAuthentication auth = new NtlmPasswordAuthentication(null, nodeUsername, nodePassword);
180         try (SmbFileInputStream in = new SmbFileInputStream(new SmbFile(url, auth))) {
181             return IOUtils.toString(in, StandardCharsets.UTF_8.name());
182         }
183     }
184
185     private void updateChannel(String channelId, boolean force) {
186         if (nodeData != null && (force || isLinked(channelId))) {
187             State state = getChannelState(channelId, nodeData);
188             logger.debug("Update channel {} with state {}", channelId, state);
189             updateState(channelId, state);
190         }
191     }
192
193     private State getChannelState(String channelId, NodeData nodeData) {
194         State state = UnDefType.UNDEF;
195
196         // Handle system channel IDs separately, because 'switch/case' expressions must be constant expressions
197         if (CHANNEL_BATTERY_LEVEL.equals(channelId)) {
198             state = new DecimalType(BigDecimal.valueOf(nodeData.getStatus().getBattery()).longValue());
199         } else if (CHANNEL_WIFI_STRENGTH.equals(channelId)) {
200             state = new DecimalType(
201                     BigDecimal.valueOf(Math.max(0, nodeData.getStatus().getWifiStrength() - 1)).longValue());
202         } else {
203             // Handle binding-specific channel IDs
204             switch (channelId) {
205                 case CHANNEL_CO2:
206                     state = new QuantityType<>(nodeData.getMeasurements().getCo2Ppm(), PARTS_PER_MILLION);
207                     break;
208                 case CHANNEL_HUMIDITY:
209                     state = new QuantityType<>(nodeData.getMeasurements().getHumidityRH(), PERCENT);
210                     break;
211                 case CHANNEL_AQI_US:
212                     state = new QuantityType<>(nodeData.getMeasurements().getPm25AQIUS(), ONE);
213                     break;
214                 case CHANNEL_PM_25:
215                     // PM2.5 is in ug/m3
216                     state = new QuantityType<>(nodeData.getMeasurements().getPm25Ugm3(),
217                             MICRO(GRAM).divide(CUBIC_METRE));
218                     break;
219                 case CHANNEL_TEMP_CELSIUS:
220                     state = new QuantityType<>(nodeData.getMeasurements().getTemperatureC(), CELSIUS);
221                     break;
222                 case CHANNEL_TIMESTAMP:
223                     // It seem the Node timestamp is Unix timestamp converted from UTC time plus timezone offset.
224                     // Not sure about DST though, but it's best guess at now
225                     Instant instant = Instant.ofEpochMilli(nodeData.getStatus().getDatetime() * 1000L);
226                     ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"));
227                     ZoneId zoneId = ZoneId.of(nodeData.getSettings().getTimezone());
228                     ZoneRules zoneRules = zoneId.getRules();
229                     zonedDateTime.minus(Duration.ofSeconds(zoneRules.getOffset(instant).getTotalSeconds()));
230                     if (zoneRules.isDaylightSavings(instant)) {
231                         zonedDateTime.minus(Duration.ofSeconds(zoneRules.getDaylightSavings(instant).getSeconds()));
232                     }
233                     state = new DateTimeType(zonedDateTime);
234                     break;
235                 case CHANNEL_USED_MEMORY:
236                     state = new DecimalType(BigDecimal.valueOf(nodeData.getStatus().getUsedMemory()).longValue());
237                     break;
238             }
239         }
240
241         return state;
242     }
243 }