2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.gpstracker.internal.handler;
15 import static org.openhab.binding.gpstracker.internal.GPSTrackerBindingConstants.*;
16 import static org.openhab.binding.gpstracker.internal.config.ConfigHelper.CONFIG_REGION_CENTER_LOCATION;
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
23 import java.util.function.Function;
24 import java.util.stream.Collectors;
26 import javax.measure.Unit;
27 import javax.measure.quantity.Length;
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;
59 * The {@link TrackerHandler} class is a tracker thing handler.
61 * @author Gabor Bicskei - Initial contribution
64 public class TrackerHandler extends BaseThingHandler {
68 private static final String EVENT_ENTER = "enter";
69 private static final String EVENT_LEAVE = "leave";
74 private final Logger logger = LoggerFactory.getLogger(TrackerHandler.class);
77 * Notification handler
79 private NotificationHandler notificationHandler;
84 private NotificationBroker notificationBroker;
87 * Id of the tracker represented by the thing
89 private String trackerId;
92 * Map of regionName/distance channels
94 private Map<String, Channel> distanceChannelMap = new HashMap<>();
97 * Map of last trigger events per region
99 private Map<String, Boolean> lastTriggeredStates = new HashMap<>();
102 * Set of all regions referenced by distance channels and extended by the received transition messages.
104 private Set<String> regions;
109 private @Nullable PointType sysLocation;
114 private @Nullable UnitProvider unitProvider;
117 * Last message received from the tracker
119 private @Nullable LocationMessage lastMessage;
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
130 public TrackerHandler(Thing thing, NotificationBroker notificationBroker, Set<String> regions,
131 @Nullable PointType sysLocation, @Nullable UnitProvider unitProvider) {
134 this.notificationBroker = notificationBroker;
135 this.notificationHandler = new NotificationHandler();
136 this.regions = regions;
137 this.sysLocation = sysLocation;
138 this.unitProvider = unitProvider;
140 trackerId = ConfigHelper.getTrackerId(thing.getConfiguration());
141 notificationBroker.registerHandler(trackerId, notificationHandler);
143 logger.debug("Tracker handler created: {}", trackerId);
147 * Returns tracker id configuration of the thing.
151 public String getTrackerId() {
156 public void initialize() {
157 if (sysLocation != null) {
158 createBasicDistanceChannel();
160 logger.debug("System location is not set. Skipping system distance channel setup.");
163 mapDistanceChannels();
164 updateStatus(ThingStatus.ONLINE);
168 * Create distance channel for measuring the distance between the tracker and the szstem.
170 private void createBasicDistanceChannel() {
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: {}",
184 channelBuilder = callback.editChannel(thing, systemDistanceChannelUID);
185 Configuration configToUpdate = systemDistance.getConfiguration();
186 configToUpdate.put(CONFIG_REGION_CENTER_LOCATION, sysLocationString);
187 channelBuilder.withConfiguration(configToUpdate);
189 logger.trace("Existing distance channel for system. No change.");
192 logger.trace("Creating missing distance channel for system.");
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);
200 channelBuilder = callback.createChannelBuilder(systemDistanceChannelUID, CHANNEL_TYPE_DISTANCE)
201 .withLabel("System Distance").withConfiguration(config);
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);
210 channels.add(channelBuilder.build());
212 ThingBuilder thingBuilder = editThing();
213 thingBuilder.withChannels(channels);
214 updateThing(thingBuilder.build());
216 logger.debug("Distance channel created for system: {}", systemDistanceChannelUID);
222 * Create a map of all configured distance channels to handle channel updates easily.
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());
233 public void handleCommand(ChannelUID channelUID, Command command) {
234 LocationMessage lastMessageLocal = lastMessage;
235 if (command instanceof RefreshType && lastMessageLocal != null) {
236 String channelId = channelUID.getId();
238 case CHANNEL_LAST_REPORT:
239 updateBaseChannels(lastMessageLocal, CHANNEL_LAST_REPORT);
241 case CHANNEL_LAST_LOCATION:
242 updateBaseChannels(lastMessageLocal, CHANNEL_LAST_LOCATION);
244 case CHANNEL_BATTERY_LEVEL:
245 updateBaseChannels(lastMessageLocal, CHANNEL_BATTERY_LEVEL);
247 case CHANNEL_GPS_ACCURACY:
248 updateBaseChannels(lastMessageLocal, CHANNEL_GPS_ACCURACY);
250 default: // distance channels
252 Channel channel = thing.getChannel(channelId);
253 if (channel != null) {
254 updateDistanceChannelFromMessage(lastMessageLocal, channel);
261 * Handle transition messages by firing the trigger channel with regionName/event payload.
263 * @param message TransitionMessage message.
265 private void updateTriggerChannelsWithTransition(TransitionMessage message) {
266 String regionName = message.getRegionName();
267 triggerRegionChannel(regionName, message.getEvent(), true);
271 * Fire trigger event with regionName/enter|leave payload but only if the event differs from the last event.
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.
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);
286 lastTriggeredStates.put(regionName, newState);
290 * Update state channels from location message. This includes basic channel updates and recalculations of all
293 * @param message Message.
295 private void updateChannelsWithLocation(LocationMessage message) {
296 updateBaseChannels(message, CHANNEL_BATTERY_LEVEL, CHANNEL_LAST_LOCATION, CHANNEL_LAST_REPORT,
297 CHANNEL_GPS_ACCURACY);
299 String trackerId = message.getTrackerId();
300 logger.debug("Updating distance channels tracker {}", trackerId);
301 distanceChannelMap.values().forEach(c -> updateDistanceChannelFromMessage(message, c));
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()
312 if (accuracyThreshold >= accuracy || accuracyThreshold.intValue() == 0) {
313 if (accuracyThreshold > 0) {
314 logger.debug("Location accuracy is below required threshold: {}<={}", accuracy, accuracyThreshold);
316 logger.debug("Location accuracy threshold check is disabled.");
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,
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);
335 triggerRegionChannel(regionName, EVENT_LEAVE, false);
340 logger.debug("Skip update as location accuracy is above required threshold: {}>{}", accuracy,
345 private double convertToMeters(double valueToConvert) {
346 UnitProvider unitProviderLocal = unitProvider;
347 if (unitProviderLocal != null) {
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);
355 logger.trace("System uses SI measurement units. No conversion is needed.");
358 logger.trace("No unit provider. Considering region radius {} in meters.", valueToConvert);
360 return valueToConvert;
364 * Update basic channels: batteryLevel, lastLocation, lastReport
366 * @param message Received message.
368 private void updateBaseChannels(LocationMessage message, String... channels) {
369 logger.debug("Update base channels for tracker {} from message: {}", trackerId, message);
371 for (String channel : channels) {
373 case CHANNEL_LAST_REPORT:
374 State timestamp = message.getTimestamp();
375 updateState(CHANNEL_LAST_REPORT, timestamp);
376 logger.trace("{} -> {}", CHANNEL_LAST_REPORT, timestamp);
378 case CHANNEL_LAST_LOCATION:
379 State newLocation = message.getTrackerLocation();
380 updateState(CHANNEL_LAST_LOCATION, newLocation);
381 logger.trace("{} -> {}", CHANNEL_LAST_LOCATION, newLocation);
383 case CHANNEL_BATTERY_LEVEL:
384 State batteryLevel = message.getBatteryLevel();
385 updateState(CHANNEL_BATTERY_LEVEL, batteryLevel);
386 logger.trace("{} -> {}", CHANNEL_BATTERY_LEVEL, batteryLevel);
388 case CHANNEL_GPS_ACCURACY:
389 State accuracy = message.getGpsAccuracy();
390 updateState(CHANNEL_GPS_ACCURACY, accuracy);
391 logger.trace("{} -> {}", CHANNEL_GPS_ACCURACY, accuracy);
398 * Location message handling.
400 * @param lm Location message
402 public void updateLocation(LocationMessage lm) {
403 this.lastMessage = lm;
404 updateStatus(ThingStatus.ONLINE);
405 updateChannelsWithLocation(lm);
406 notificationBroker.sendNotification(lm);
410 * Transition message handling
412 * @param tm Transition message
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);
421 updateChannelsWithLocation(tm);
422 updateTriggerChannelsWithTransition(tm);
424 notificationBroker.sendNotification(tm);
428 * Get notification to return to the tracker (supported by OwnTracks only)
430 * @return List of notifications received from other trackers
432 public List<LocationMessage> getNotifications() {
433 return notificationHandler.getNotifications();