]> git.basschouten.com Git - openhab-addons.git/blob
0f12ab127e4da19931eee2c4d6421b6b687d321e
[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.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(), true);
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      * @param forced Force channel triggering in case the transition event is received from the mobile application.
274      */
275     private void triggerRegionChannel(@NonNull String regionName, @NonNull String event, boolean forced) {
276         Boolean lastState = lastTriggeredStates.get(regionName);
277         Boolean newState = EVENT_ENTER.equals(event);
278         if (!newState.equals(lastState) || forced) {
279             String payload = regionName + "/" + event;
280             triggerChannel(CHANNEL_REGION_TRIGGER, payload);
281             lastTriggeredStates.put(regionName, newState);
282             logger.trace("Triggering {} for {}/{}", regionName, trackerId, payload);
283         }
284         lastTriggeredStates.put(regionName, newState);
285     }
286
287     /**
288      * Update state channels from location message. This includes basic channel updates and recalculations of all
289      * distances.
290      *
291      * @param message Message.
292      */
293     private void updateChannelsWithLocation(LocationMessage message) {
294         updateBaseChannels(message, CHANNEL_BATTERY_LEVEL, CHANNEL_LAST_LOCATION, CHANNEL_LAST_REPORT,
295                 CHANNEL_GPS_ACCURACY);
296
297         String trackerId = message.getTrackerId();
298         logger.debug("Updating distance channels tracker {}", trackerId);
299         distanceChannelMap.values().forEach(c -> updateDistanceChannelFromMessage(message, c));
300     }
301
302     private void updateDistanceChannelFromMessage(LocationMessage message, Channel c) {
303         Configuration currentConfig = c.getConfiguration();
304         // convert into meters which is the unit of the threshold
305         Double accuracyThreshold = convertToMeters(ConfigHelper.getAccuracyThreshold(currentConfig));
306         State messageAccuracy = message.getGpsAccuracy();
307         Double accuracy = messageAccuracy != UnDefType.UNDEF ? ((QuantityType<?>) messageAccuracy).doubleValue()
308                 : accuracyThreshold;
309
310         if (accuracyThreshold >= accuracy || accuracyThreshold.intValue() == 0) {
311             if (accuracyThreshold > 0) {
312                 logger.debug("Location accuracy is below required threshold: {}<={}", accuracy, accuracyThreshold);
313             } else {
314                 logger.debug("Location accuracy threshold check is disabled.");
315             }
316
317             String regionName = ConfigHelper.getRegionName(currentConfig);
318             PointType center = ConfigHelper.getRegionCenterLocation(currentConfig);
319             State newLocation = message.getTrackerLocation();
320             if (center != null && newLocation != UnDefType.UNDEF) {
321                 double newDistance = center.distanceFrom((PointType) newLocation).doubleValue();
322                 updateState(c.getUID(), new QuantityType<>(newDistance / 1000, MetricPrefix.KILO(SIUnits.METRE)));
323                 logger.trace("Region {} center distance from tracker location {} is {}m", regionName, newLocation,
324                         newDistance);
325
326                 // fire trigger based on distance calculation only in case of pure location message
327                 if (!(message instanceof TransitionMessage)) {
328                     // convert into meters which is the unit of the calculated distance
329                     double radiusMeter = convertToMeters(ConfigHelper.getRegionRadius(c.getConfiguration()));
330                     if (radiusMeter > newDistance) {
331                         triggerRegionChannel(regionName, EVENT_ENTER, false);
332                     } else {
333                         triggerRegionChannel(regionName, EVENT_LEAVE, false);
334                     }
335                 }
336             }
337         } else {
338             logger.debug("Skip update as location accuracy is above required threshold: {}>{}", accuracy,
339                     accuracyThreshold);
340         }
341     }
342
343     private double convertToMeters(double valueToConvert) {
344         if (unitProvider != null) {
345             @Nullable
346             Unit<Length> unit = unitProvider.getUnit(Length.class);
347             if (unit != null && !SIUnits.METRE.equals(unit)) {
348                 double value = ImperialUnits.YARD.getConverterTo(SIUnits.METRE).convert(valueToConvert);
349                 logger.trace("Value converted: {}yd->{}m", valueToConvert, value);
350                 return value;
351             } else {
352                 logger.trace("System uses SI measurement units. No conversion is needed.");
353             }
354         } else {
355             logger.trace("No unit provider. Considering region radius {} in meters.", valueToConvert);
356         }
357         return valueToConvert;
358     }
359
360     /**
361      * Update basic channels: batteryLevel, lastLocation, lastReport
362      *
363      * @param message Received message.
364      */
365     private void updateBaseChannels(LocationMessage message, String... channels) {
366         logger.debug("Update base channels for tracker {} from message: {}", trackerId, message);
367
368         for (String channel : channels) {
369             switch (channel) {
370                 case CHANNEL_LAST_REPORT:
371                     State timestamp = message.getTimestamp();
372                     updateState(CHANNEL_LAST_REPORT, timestamp);
373                     logger.trace("{} -> {}", CHANNEL_LAST_REPORT, timestamp);
374                     break;
375                 case CHANNEL_LAST_LOCATION:
376                     State newLocation = message.getTrackerLocation();
377                     updateState(CHANNEL_LAST_LOCATION, newLocation);
378                     logger.trace("{} -> {}", CHANNEL_LAST_LOCATION, newLocation);
379                     break;
380                 case CHANNEL_BATTERY_LEVEL:
381                     State batteryLevel = message.getBatteryLevel();
382                     updateState(CHANNEL_BATTERY_LEVEL, batteryLevel);
383                     logger.trace("{} -> {}", CHANNEL_BATTERY_LEVEL, batteryLevel);
384                     break;
385                 case CHANNEL_GPS_ACCURACY:
386                     State accuracy = message.getGpsAccuracy();
387                     updateState(CHANNEL_GPS_ACCURACY, accuracy);
388                     logger.trace("{} -> {}", CHANNEL_GPS_ACCURACY, accuracy);
389                     break;
390             }
391         }
392     }
393
394     /**
395      * Location message handling.
396      *
397      * @param lm Location message
398      */
399     public void updateLocation(LocationMessage lm) {
400         this.lastMessage = lm;
401         updateStatus(ThingStatus.ONLINE);
402         updateChannelsWithLocation(lm);
403         notificationBroker.sendNotification(lm);
404     }
405
406     /**
407      * Transition message handling
408      *
409      * @param tm Transition message
410      */
411     public void doTransition(TransitionMessage tm) {
412         this.lastMessage = tm;
413         updateStatus(ThingStatus.ONLINE);
414         String regionName = tm.getRegionName();
415         logger.debug("ConfigHelper transition event received: {}", regionName);
416         regions.add(regionName);
417
418         updateChannelsWithLocation(tm);
419         updateTriggerChannelsWithTransition(tm);
420
421         notificationBroker.sendNotification(tm);
422     }
423
424     /**
425      * Get notification to return to the tracker (supported by OwnTracks only)
426      *
427      * @return List of notifications received from other trackers
428      */
429     public List<LocationMessage> getNotifications() {
430         return notificationHandler.getNotifications();
431     }
432 }