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