]> git.basschouten.com Git - openhab-addons.git/blob
f6fd19771addae6edbff7d4e60b440678fe1d499
[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.mihome.internal.handler;
14
15 import static org.openhab.binding.mihome.internal.XiaomiGatewayBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.*;
17
18 import java.util.Arrays;
19 import java.util.HashSet;
20 import java.util.Set;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
24 import javax.measure.Unit;
25 import javax.measure.quantity.Angle;
26 import javax.measure.quantity.Dimensionless;
27 import javax.measure.quantity.Pressure;
28 import javax.measure.quantity.Temperature;
29 import javax.measure.quantity.Time;
30
31 import org.openhab.binding.mihome.internal.XiaomiItemUpdateListener;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.library.unit.SIUnits;
34 import org.openhab.core.library.unit.Units;
35 import org.openhab.core.thing.Bridge;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.ThingTypeUID;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.thing.binding.ThingHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 import com.google.gson.JsonObject;
49 import com.google.gson.JsonParser;
50 import com.google.gson.JsonSyntaxException;
51
52 /**
53  * The {@link XiaomiDeviceBaseHandler} is responsible for handling commands, which are
54  * sent to one of the channels.
55  *
56  * @author Patrick Boos - Initial contribution
57  * @author Kuba Wolanin - Added voltage and low battery report
58  * @author Dieter Schmidt - Added cube rotation, heartbeat and voltage handling, configurable window and motion delay,
59  *         Aqara
60  *         switches
61  * @author Daniel Walters - Added Aqara Door/Window sensor and Aqara temperature, humidity and pressure sensor
62  */
63 public class XiaomiDeviceBaseHandler extends BaseThingHandler implements XiaomiItemUpdateListener {
64
65     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = new HashSet<>(Arrays.asList(THING_TYPE_GATEWAY,
66             THING_TYPE_SENSOR_HT, THING_TYPE_SENSOR_AQARA_WEATHER_V1, THING_TYPE_SENSOR_MOTION,
67             THING_TYPE_SENSOR_AQARA_MOTION, THING_TYPE_SENSOR_SWITCH, THING_TYPE_SENSOR_AQARA_SWITCH,
68             THING_TYPE_SENSOR_MAGNET, THING_TYPE_SENSOR_AQARA_LOCK, THING_TYPE_SENSOR_AQARA_MAGNET,
69             THING_TYPE_SENSOR_CUBE, THING_TYPE_SENSOR_AQARA_VIBRATION, THING_TYPE_SENSOR_AQARA1,
70             THING_TYPE_SENSOR_AQARA2, THING_TYPE_SENSOR_GAS, THING_TYPE_SENSOR_SMOKE, THING_TYPE_SENSOR_WATER,
71             THING_TYPE_ACTOR_AQARA1, THING_TYPE_ACTOR_AQARA2, THING_TYPE_ACTOR_PLUG, THING_TYPE_ACTOR_AQARA_ZERO1,
72             THING_TYPE_ACTOR_AQARA_ZERO2, THING_TYPE_ACTOR_CURTAIN, THING_TYPE_BASIC));
73
74     protected static final Unit<Temperature> TEMPERATURE_UNIT = SIUnits.CELSIUS;
75     protected static final Unit<Pressure> PRESSURE_UNIT = KILO(SIUnits.PASCAL);
76     protected static final Unit<Dimensionless> PERCENT_UNIT = Units.PERCENT;
77     protected static final Unit<Angle> ANGLE_UNIT = Units.DEGREE_ANGLE;
78     protected static final Unit<Time> TIME_UNIT = MILLI(Units.SECOND);
79
80     private static final String REMOVE_DEVICE = "remove_device";
81     private static final long ONLINE_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(2);
82     private ScheduledFuture<?> onlineCheckTask;
83
84     private XiaomiBridgeHandler bridgeHandler;
85
86     private String itemId;
87
88     private final void setItemId(String itemId) {
89         this.itemId = itemId;
90     }
91
92     private final Logger logger = LoggerFactory.getLogger(XiaomiDeviceBaseHandler.class);
93
94     public XiaomiDeviceBaseHandler(Thing thing) {
95         super(thing);
96     }
97
98     @Override
99     public void initialize() {
100         setItemId((String) getConfig().get(ITEM_ID));
101         onlineCheckTask = scheduler.scheduleWithFixedDelay(this::updateThingStatus, 0, ONLINE_TIMEOUT_MILLIS / 2,
102                 TimeUnit.MILLISECONDS);
103     }
104
105     @Override
106     public void dispose() {
107         logger.debug("Handler disposes. Unregistering listener");
108         if (getItemId() != null) {
109             if (bridgeHandler != null) {
110                 bridgeHandler.unregisterItemListener(this);
111                 bridgeHandler = null;
112             }
113             setItemId(null);
114         }
115         if (!onlineCheckTask.isDone()) {
116             onlineCheckTask.cancel(false);
117         }
118     }
119
120     @Override
121     public void handleRemoval() {
122         getXiaomiBridgeHandler().writeToBridge(new String[] { REMOVE_DEVICE }, new Object[] { itemId });
123         super.handleRemoval();
124     }
125
126     @Override
127     public void handleCommand(ChannelUID channelUID, Command command) {
128         logger.debug("Device {} on channel {} received command {}", getItemId(), channelUID, command);
129         if (command instanceof RefreshType) {
130             JsonObject message = getXiaomiBridgeHandler().getDeferredMessage(getItemId());
131             if (message != null) {
132                 String cmd = message.get("cmd").getAsString();
133                 logger.debug("Update Item {} with retented message", getItemId());
134                 onItemUpdate(getItemId(), cmd, message);
135             }
136             return;
137         }
138         execute(channelUID, command);
139     }
140
141     @Override
142     public void onItemUpdate(String sid, String command, JsonObject message) {
143         if (getItemId() != null && getItemId().equals(sid)) {
144             if (getThing().getStatus() != ThingStatus.ONLINE) {
145                 updateStatus(ThingStatus.ONLINE);
146             }
147             logger.debug("Item got update: {}", message);
148             try {
149                 JsonObject data = JsonParser.parseString(message.get("data").getAsString()).getAsJsonObject();
150                 parseCommand(command, data);
151                 if (THING_TYPE_BASIC.equals(getThing().getThingTypeUID())) {
152                     parseDefault(message);
153                 }
154             } catch (JsonSyntaxException e) {
155                 logger.warn("Unable to parse message as valid JSON: {}", message);
156             }
157         }
158     }
159
160     @Override
161     public String getItemId() {
162         return itemId;
163     }
164
165     void parseCommand(String command, JsonObject data) {
166         switch (command) {
167             case "report":
168                 parseReport(data);
169                 break;
170             case "heartbeat":
171                 parseHeartbeat(data);
172                 break;
173             case "read_ack":
174                 parseReadAck(data);
175                 break;
176             case "write_ack":
177                 parseWriteAck(data);
178                 break;
179             default:
180                 logger.debug("Device {} got unknown command {}", getItemId(), command);
181         }
182     }
183
184     void parseReport(JsonObject data) {
185         updateState(CHANNEL_REPORT_MSG, StringType.valueOf(data.toString()));
186     }
187
188     void parseHeartbeat(JsonObject data) {
189         updateState(CHANNEL_HEARTBEAT_MSG, StringType.valueOf(data.toString()));
190     }
191
192     void parseReadAck(JsonObject data) {
193         updateState(CHANNEL_READ_ACK_MSG, StringType.valueOf(data.toString()));
194     }
195
196     void parseWriteAck(JsonObject data) {
197         updateState(CHANNEL_WRITE_ACK_MSG, StringType.valueOf(data.toString()));
198     }
199
200     void parseDefault(JsonObject data) {
201         updateState(CHANNEL_LAST_MSG, StringType.valueOf(data.toString()));
202     }
203
204     void execute(ChannelUID channelUID, Command command) {
205         if (CHANNEL_WRITE_MSG.equals(channelUID.getId())) {
206             if (command instanceof StringType) {
207                 getXiaomiBridgeHandler().writeToDevice(itemId, ((StringType) command).toFullString());
208             } else {
209                 logger.debug("Command \"{}\" has to be of StringType", command);
210             }
211         } else {
212             logger.debug("Received command on read-only channel, thus ignoring it.");
213         }
214     }
215
216     private void updateThingStatus() {
217         if (getItemId() != null) {
218             // note: this call implicitly registers our handler as a listener on the bridge, if it's not already
219             if (getXiaomiBridgeHandler() != null) {
220                 Bridge bridge = getBridge();
221                 ThingStatus bridgeStatus = (bridge == null) ? null : bridge.getStatus();
222                 if (bridgeStatus == ThingStatus.ONLINE) {
223                     ThingStatus itemStatus = getThing().getStatus();
224                     boolean hasItemActivity = getXiaomiBridgeHandler().hasItemActivity(getItemId(),
225                             ONLINE_TIMEOUT_MILLIS);
226                     ThingStatus newStatus = hasItemActivity ? ThingStatus.ONLINE : ThingStatus.OFFLINE;
227
228                     if (!newStatus.equals(itemStatus)) {
229                         updateStatus(newStatus);
230                     }
231                 } else {
232                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
233                 }
234             } else {
235                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
236             }
237         } else {
238             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
239         }
240     }
241
242     synchronized XiaomiBridgeHandler getXiaomiBridgeHandler() {
243         if (this.bridgeHandler == null) {
244             Bridge bridge = getBridge();
245             if (bridge == null) {
246                 return null;
247             }
248             ThingHandler handler = bridge.getHandler();
249             if (handler instanceof XiaomiBridgeHandler) {
250                 this.bridgeHandler = (XiaomiBridgeHandler) handler;
251                 this.bridgeHandler.registerItemListener(this);
252             } else {
253                 return null;
254             }
255         }
256         return this.bridgeHandler;
257     }
258 }