]> git.basschouten.com Git - openhab-addons.git/blob
9d4ede111d19598a51ebf8d470393773e54d82fd
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.gpstracker.internal.handler;
14
15 import static org.openhab.binding.gpstracker.internal.GPSTrackerBindingConstants.*;
16 import static org.openhab.binding.gpstracker.internal.config.ConfigHelper.CONFIG_REGION_CENTER_LOCATION;
17
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.function.Function;
24 import java.util.stream.Collectors;
25
26 import javax.measure.Unit;
27 import javax.measure.quantity.Length;
28
29 import org.eclipse.jdt.annotation.NonNull;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.gpstracker.internal.config.ConfigHelper;
32 import org.openhab.binding.gpstracker.internal.message.LocationMessage;
33 import org.openhab.binding.gpstracker.internal.message.NotificationBroker;
34 import org.openhab.binding.gpstracker.internal.message.NotificationHandler;
35 import org.openhab.binding.gpstracker.internal.message.TransitionMessage;
36 import org.openhab.core.config.core.Configuration;
37 import org.openhab.core.i18n.UnitProvider;
38 import org.openhab.core.library.types.PointType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.unit.ImperialUnits;
41 import org.openhab.core.library.unit.MetricPrefix;
42 import org.openhab.core.library.unit.SIUnits;
43 import org.openhab.core.thing.Channel;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.thing.binding.ThingHandlerCallback;
49 import org.openhab.core.thing.binding.builder.ChannelBuilder;
50 import org.openhab.core.thing.binding.builder.ThingBuilder;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.State;
54 import org.openhab.core.types.UnDefType;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * The {@link TrackerHandler} class is a tracker thing handler.
60  *
61  * @author Gabor Bicskei - Initial contribution
62  */
63 public class TrackerHandler extends BaseThingHandler {
64     /**
65      * Trigger events
66      */
67     private static final String EVENT_ENTER = "enter";
68     private static final String EVENT_LEAVE = "leave";
69
70     /**
71      * Class logger
72      */
73     private final Logger logger = LoggerFactory.getLogger(TrackerHandler.class);
74
75     /**
76      * Notification handler
77      */
78     private NotificationHandler notificationHandler;
79
80     /**
81      * Notification broker
82      */
83     private NotificationBroker notificationBroker;
84
85     /**
86      * Id of the tracker represented by the thing
87      */
88     private String trackerId;
89
90     /**
91      * Map of regionName/distance channels
92      */
93     private Map<String, Channel> distanceChannelMap = new HashMap<>();
94
95     /**
96      * Map of last trigger events per region
97      */
98     private Map<String, Boolean> lastTriggeredStates = new HashMap<>();
99
100     /**
101      * Set of all regions referenced by distance channels and extended by the received transition messages.
102      */
103     private Set<String> regions;
104
105     /**
106      * System location
107      */
108     private PointType sysLocation;
109
110     /**
111      * Unit provider
112      */
113     private UnitProvider unitProvider;
114
115     /**
116      * Last message received from the tracker
117      */
118     private LocationMessage lastMessage;
119
120     /**
121      * Constructor.
122      *
123      * @param thing Thing.
124      * @param notificationBroker Notification broker
125      * @param regions Global region set
126      * @param sysLocation Location of the system
127      * @param unitProvider Unit provider
128      */
129     public TrackerHandler(Thing thing, NotificationBroker notificationBroker, Set<String> regions,
130             PointType sysLocation, UnitProvider unitProvider) {
131         super(thing);
132
133         this.notificationBroker = notificationBroker;
134         this.notificationHandler = new NotificationHandler();
135         this.regions = regions;
136         this.sysLocation = sysLocation;
137         this.unitProvider = unitProvider;
138
139         trackerId = ConfigHelper.getTrackerId(thing.getConfiguration());
140         notificationBroker.registerHandler(trackerId, notificationHandler);
141
142         logger.debug("Tracker handler created: {}", trackerId);
143     }
144
145     /**
146      * Returns tracker id configuration of the thing.
147      *
148      * @return Tracker id
149      */
150     public String getTrackerId() {
151         return trackerId;
152     }
153
154     @Override
155     public void initialize() {
156         if (sysLocation != null) {
157             createBasicDistanceChannel();
158         } else {
159             logger.debug("System location is not set. Skipping system distance channel setup.");
160         }
161
162         mapDistanceChannels();
163         updateStatus(ThingStatus.ONLINE);
164     }
165
166     /**
167      * Create distance channel for measuring the distance between the tracker and the szstem.
168      */
169     private void createBasicDistanceChannel() {
170         @Nullable
171         ThingHandlerCallback callback = getCallback();
172         if (callback != null) {
173             // find the system distance channel
174             ChannelUID systemDistanceChannelUID = new ChannelUID(thing.getUID(), CHANNEL_DISTANCE_SYSTEM_ID);
175             Channel systemDistance = thing.getChannel(CHANNEL_DISTANCE_SYSTEM_ID);
176             ChannelBuilder channelBuilder = null;
177             if (systemDistance != null) {
178                 if (!systemDistance.getConfiguration().get(CONFIG_REGION_CENTER_LOCATION)
179                         .equals(sysLocation.toFullString())) {
180                     logger.trace("Existing distance channel for system. Changing system location config parameter: {}",
181                             sysLocation.toFullString());
182
183                     channelBuilder = callback.editChannel(thing, systemDistanceChannelUID);
184                     Configuration configToUpdate = systemDistance.getConfiguration();
185                     configToUpdate.put(CONFIG_REGION_CENTER_LOCATION, sysLocation.toFullString());
186                     channelBuilder.withConfiguration(configToUpdate);
187                 } else {
188                     logger.trace("Existing distance channel for system. No change.");
189                 }
190             } else {
191                 logger.trace("Creating missing distance channel for system.");
192
193                 Configuration config = new Configuration();
194                 config.put(ConfigHelper.CONFIG_REGION_NAME, CHANNEL_DISTANCE_SYSTEM_NAME);
195                 config.put(CONFIG_REGION_CENTER_LOCATION, sysLocation.toFullString());
196                 config.put(ConfigHelper.CONFIG_REGION_RADIUS, CHANNEL_DISTANCE_SYSTEM_RADIUS);
197                 config.put(ConfigHelper.CONFIG_ACCURACY_THRESHOLD, 0);
198
199                 channelBuilder = callback.createChannelBuilder(systemDistanceChannelUID, CHANNEL_TYPE_DISTANCE)
200                         .withLabel("System Distance").withConfiguration(config);
201             }
202
203             // update the thing with system distance channel
204             if (channelBuilder != null) {
205                 List<Channel> channels = new ArrayList<>(thing.getChannels());
206                 if (systemDistance != null) {
207                     channels.remove(systemDistance);
208                 }
209                 channels.add(channelBuilder.build());
210
211                 ThingBuilder thingBuilder = editThing();
212                 thingBuilder.withChannels(channels);
213                 updateThing(thingBuilder.build());
214
215                 logger.debug("Distance channel created for system: {}", systemDistanceChannelUID);
216             }
217         }
218     }
219
220     /**
221      * Create a map of all configured distance channels to handle channel updates easily.
222      */
223     private void mapDistanceChannels() {
224         distanceChannelMap = thing.getChannels().stream()
225                 .filter(c -> CHANNEL_TYPE_DISTANCE.equals(c.getChannelTypeUID()))
226                 .collect(Collectors.toMap(c -> ConfigHelper.getRegionName(c.getConfiguration()), Function.identity()));
227         // register the collected regions
228         regions.addAll(distanceChannelMap.keySet());
229     }
230
231     @Override
232     public void handleCommand(ChannelUID channelUID, Command command) {
233         if (command instanceof RefreshType && lastMessage != null) {
234             String channelId = channelUID.getId();
235             switch (channelId) {
236                 case CHANNEL_LAST_REPORT:
237                     updateBaseChannels(lastMessage, CHANNEL_LAST_REPORT);
238                     break;
239                 case CHANNEL_LAST_LOCATION:
240                     updateBaseChannels(lastMessage, CHANNEL_LAST_LOCATION);
241                     break;
242                 case CHANNEL_BATTERY_LEVEL:
243                     updateBaseChannels(lastMessage, CHANNEL_BATTERY_LEVEL);
244                     break;
245                 case CHANNEL_GPS_ACCURACY:
246                     updateBaseChannels(lastMessage, CHANNEL_GPS_ACCURACY);
247                     break;
248                 default: // distance channels
249                     @Nullable
250                     Channel channel = thing.getChannel(channelId);
251                     if (channel != null) {
252                         updateDistanceChannelFromMessage(lastMessage, channel);
253                     }
254             }
255         }
256     }
257
258     /**
259      * Handle transition messages by firing the trigger channel with regionName/event payload.
260      *
261      * @param message TransitionMessage message.
262      */
263     private void updateTriggerChannelsWithTransition(TransitionMessage message) {
264         String regionName = message.getRegionName();
265         triggerRegionChannel(regionName, message.getEvent());
266     }
267
268     /**
269      * Fire trigger event with regionName/enter|leave payload but only if the event differs from the last event.
270      *
271      * @param regionName Region name
272      * @param event Occurred event
273      */
274     private void triggerRegionChannel(@NonNull String regionName, @NonNull String event) {
275         Boolean lastState = lastTriggeredStates.get(regionName);
276         Boolean newState = EVENT_ENTER.equals(event);
277         if (!newState.equals(lastState) && lastState != null) {
278             String payload = regionName + "/" + event;
279             triggerChannel(CHANNEL_REGION_TRIGGER, payload);
280             lastTriggeredStates.put(regionName, newState);
281             logger.trace("Triggering {} for {}/{}", regionName, trackerId, payload);
282         }
283         lastTriggeredStates.put(regionName, newState);
284     }
285
286     /**
287      * Update state channels from location message. This includes basic channel updates and recalculations of all
288      * distances.
289      *
290      * @param message Message.
291      */
292     private void updateChannelsWithLocation(LocationMessage message) {
293         updateBaseChannels(message, CHANNEL_BATTERY_LEVEL, CHANNEL_LAST_LOCATION, CHANNEL_LAST_REPORT,
294                 CHANNEL_GPS_ACCURACY);
295
296         String trackerId = message.getTrackerId();
297         logger.debug("Updating distance channels tracker {}", trackerId);
298         distanceChannelMap.values().forEach(c -> updateDistanceChannelFromMessage(message, c));
299     }
300
301     private void updateDistanceChannelFromMessage(LocationMessage message, Channel c) {
302         Configuration currentConfig = c.getConfiguration();
303         // convert into meters which is the unit of the threshold
304         Double accuracyThreshold = convertToMeters(ConfigHelper.getAccuracyThreshold(currentConfig));
305         State messageAccuracy = message.getGpsAccuracy();
306         Double accuracy = messageAccuracy != UnDefType.UNDEF ? ((QuantityType<?>) messageAccuracy).doubleValue()
307                 : accuracyThreshold;
308
309         if (accuracyThreshold >= accuracy || accuracyThreshold.intValue() == 0) {
310             if (accuracyThreshold > 0) {
311                 logger.debug("Location accuracy is below required threshold: {}<={}", accuracy, accuracyThreshold);
312             } else {
313                 logger.debug("Location accuracy threshold check is disabled.");
314             }
315
316             String regionName = ConfigHelper.getRegionName(currentConfig);
317             PointType center = ConfigHelper.getRegionCenterLocation(currentConfig);
318             State newLocation = message.getTrackerLocation();
319             if (center != null && newLocation != UnDefType.UNDEF) {
320                 double newDistance = center.distanceFrom((PointType) newLocation).doubleValue();
321                 updateState(c.getUID(), new QuantityType<>(newDistance / 1000, MetricPrefix.KILO(SIUnits.METRE)));
322                 logger.trace("Region {} center distance from tracker location {} is {}m", regionName, newLocation,
323                         newDistance);
324
325                 // fire trigger based on distance calculation only in case of pure location message
326                 if (!(message instanceof TransitionMessage)) {
327                     // convert into meters which is the unit of the calculated distance
328                     double radiusMeter = convertToMeters(ConfigHelper.getRegionRadius(c.getConfiguration()));
329                     if (radiusMeter > newDistance) {
330                         triggerRegionChannel(regionName, EVENT_ENTER);
331                     } else {
332                         triggerRegionChannel(regionName, EVENT_LEAVE);
333                     }
334                 }
335             }
336         } else {
337             logger.debug("Skip update as location accuracy is above required threshold: {}>{}", accuracy,
338                     accuracyThreshold);
339         }
340     }
341
342     private double convertToMeters(double valueToConvert) {
343         if (unitProvider != null) {
344             @Nullable
345             Unit<Length> unit = unitProvider.getUnit(Length.class);
346             if (unit != null && !SIUnits.METRE.equals(unit)) {
347                 double value = ImperialUnits.YARD.getConverterTo(SIUnits.METRE).convert(valueToConvert);
348                 logger.trace("Value converted: {}yd->{}m", valueToConvert, value);
349                 return value;
350             } else {
351                 logger.trace("System uses SI measurement units. No conversion is needed.");
352             }
353         } else {
354             logger.trace("No unit provider. Considering region radius {} in meters.", valueToConvert);
355         }
356         return valueToConvert;
357     }
358
359     /**
360      * Update basic channels: batteryLevel, lastLocation, lastReport
361      *
362      * @param message Received message.
363      */
364     private void updateBaseChannels(LocationMessage message, String... channels) {
365         logger.debug("Update base channels for tracker {} from message: {}", trackerId, message);
366
367         for (String channel : channels) {
368             switch (channel) {
369                 case CHANNEL_LAST_REPORT:
370                     State timestamp = message.getTimestamp();
371                     updateState(CHANNEL_LAST_REPORT, timestamp);
372                     logger.trace("{} -> {}", CHANNEL_LAST_REPORT, timestamp);
373                     break;
374                 case CHANNEL_LAST_LOCATION:
375                     State newLocation = message.getTrackerLocation();
376                     updateState(CHANNEL_LAST_LOCATION, newLocation);
377                     logger.trace("{} -> {}", CHANNEL_LAST_LOCATION, newLocation);
378                     break;
379                 case CHANNEL_BATTERY_LEVEL:
380                     State batteryLevel = message.getBatteryLevel();
381                     updateState(CHANNEL_BATTERY_LEVEL, batteryLevel);
382                     logger.trace("{} -> {}", CHANNEL_BATTERY_LEVEL, batteryLevel);
383                     break;
384                 case CHANNEL_GPS_ACCURACY:
385                     State accuracy = message.getGpsAccuracy();
386                     updateState(CHANNEL_GPS_ACCURACY, accuracy);
387                     logger.trace("{} -> {}", CHANNEL_GPS_ACCURACY, accuracy);
388                     break;
389             }
390         }
391     }
392
393     /**
394      * Location message handling.
395      *
396      * @param lm Location message
397      */
398     public void updateLocation(LocationMessage lm) {
399         this.lastMessage = lm;
400         updateStatus(ThingStatus.ONLINE);
401         updateChannelsWithLocation(lm);
402         notificationBroker.sendNotification(lm);
403     }
404
405     /**
406      * Transition message handling
407      *
408      * @param tm Transition message
409      */
410     public void doTransition(TransitionMessage tm) {
411         this.lastMessage = tm;
412         updateStatus(ThingStatus.ONLINE);
413         String regionName = tm.getRegionName();
414         logger.debug("ConfigHelper transition event received: {}", regionName);
415         regions.add(regionName);
416
417         updateChannelsWithLocation(tm);
418         updateTriggerChannelsWithTransition(tm);
419
420         notificationBroker.sendNotification(tm);
421     }
422
423     /**
424      * Get notification to return to the tracker (supported by OwnTracks only)
425      *
426      * @return List of notifications received from other trackers
427      */
428     public List<LocationMessage> getNotifications() {
429         return notificationHandler.getNotifications();
430     }
431 }