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