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.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;
59 * The {@link TrackerHandler} class is a tracker thing handler.
61 * @author Gabor Bicskei - Initial contribution
63 public class TrackerHandler extends BaseThingHandler {
67 private static final String EVENT_ENTER = "enter";
68 private static final String EVENT_LEAVE = "leave";
73 private final Logger logger = LoggerFactory.getLogger(TrackerHandler.class);
76 * Notification handler
78 private NotificationHandler notificationHandler;
83 private NotificationBroker notificationBroker;
86 * Id of the tracker represented by the thing
88 private String trackerId;
91 * Map of regionName/distance channels
93 private Map<String, Channel> distanceChannelMap = new HashMap<>();
96 * Map of last trigger events per region
98 private Map<String, Boolean> lastTriggeredStates = new HashMap<>();
101 * Set of all regions referenced by distance channels and extended by the received transition messages.
103 private Set<String> regions;
108 private PointType sysLocation;
113 private UnitProvider unitProvider;
116 * Last message received from the tracker
118 private LocationMessage lastMessage;
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
129 public TrackerHandler(Thing thing, NotificationBroker notificationBroker, Set<String> regions,
130 PointType sysLocation, UnitProvider unitProvider) {
133 this.notificationBroker = notificationBroker;
134 this.notificationHandler = new NotificationHandler();
135 this.regions = regions;
136 this.sysLocation = sysLocation;
137 this.unitProvider = unitProvider;
139 trackerId = ConfigHelper.getTrackerId(thing.getConfiguration());
140 notificationBroker.registerHandler(trackerId, notificationHandler);
142 logger.debug("Tracker handler created: {}", trackerId);
146 * Returns tracker id configuration of the thing.
150 public String getTrackerId() {
155 public void initialize() {
156 if (sysLocation != null) {
157 createBasicDistanceChannel();
159 logger.debug("System location is not set. Skipping system distance channel setup.");
162 mapDistanceChannels();
163 updateStatus(ThingStatus.ONLINE);
167 * Create distance channel for measuring the distance between the tracker and the szstem.
169 private void createBasicDistanceChannel() {
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());
183 channelBuilder = callback.editChannel(thing, systemDistanceChannelUID);
184 Configuration configToUpdate = systemDistance.getConfiguration();
185 configToUpdate.put(CONFIG_REGION_CENTER_LOCATION, sysLocation.toFullString());
186 channelBuilder.withConfiguration(configToUpdate);
188 logger.trace("Existing distance channel for system. No change.");
191 logger.trace("Creating missing distance channel for system.");
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);
199 channelBuilder = callback.createChannelBuilder(systemDistanceChannelUID, CHANNEL_TYPE_DISTANCE)
200 .withLabel("System Distance").withConfiguration(config);
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);
209 channels.add(channelBuilder.build());
211 ThingBuilder thingBuilder = editThing();
212 thingBuilder.withChannels(channels);
213 updateThing(thingBuilder.build());
215 logger.debug("Distance channel created for system: {}", systemDistanceChannelUID);
221 * Create a map of all configured distance channels to handle channel updates easily.
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());
232 public void handleCommand(ChannelUID channelUID, Command command) {
233 if (command instanceof RefreshType && lastMessage != null) {
234 String channelId = channelUID.getId();
236 case CHANNEL_LAST_REPORT:
237 updateBaseChannels(lastMessage, CHANNEL_LAST_REPORT);
239 case CHANNEL_LAST_LOCATION:
240 updateBaseChannels(lastMessage, CHANNEL_LAST_LOCATION);
242 case CHANNEL_BATTERY_LEVEL:
243 updateBaseChannels(lastMessage, CHANNEL_BATTERY_LEVEL);
245 case CHANNEL_GPS_ACCURACY:
246 updateBaseChannels(lastMessage, CHANNEL_GPS_ACCURACY);
248 default: // distance channels
250 Channel channel = thing.getChannel(channelId);
251 if (channel != null) {
252 updateDistanceChannelFromMessage(lastMessage, channel);
259 * Handle transition messages by firing the trigger channel with regionName/event payload.
261 * @param message TransitionMessage message.
263 private void updateTriggerChannelsWithTransition(TransitionMessage message) {
264 String regionName = message.getRegionName();
265 triggerRegionChannel(regionName, message.getEvent(), true);
269 * Fire trigger event with regionName/enter|leave payload but only if the event differs from the last event.
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.
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);
284 lastTriggeredStates.put(regionName, newState);
288 * Update state channels from location message. This includes basic channel updates and recalculations of all
291 * @param message Message.
293 private void updateChannelsWithLocation(LocationMessage message) {
294 updateBaseChannels(message, CHANNEL_BATTERY_LEVEL, CHANNEL_LAST_LOCATION, CHANNEL_LAST_REPORT,
295 CHANNEL_GPS_ACCURACY);
297 String trackerId = message.getTrackerId();
298 logger.debug("Updating distance channels tracker {}", trackerId);
299 distanceChannelMap.values().forEach(c -> updateDistanceChannelFromMessage(message, c));
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()
310 if (accuracyThreshold >= accuracy || accuracyThreshold.intValue() == 0) {
311 if (accuracyThreshold > 0) {
312 logger.debug("Location accuracy is below required threshold: {}<={}", accuracy, accuracyThreshold);
314 logger.debug("Location accuracy threshold check is disabled.");
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,
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);
333 triggerRegionChannel(regionName, EVENT_LEAVE, false);
338 logger.debug("Skip update as location accuracy is above required threshold: {}>{}", accuracy,
343 private double convertToMeters(double valueToConvert) {
344 if (unitProvider != null) {
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);
352 logger.trace("System uses SI measurement units. No conversion is needed.");
355 logger.trace("No unit provider. Considering region radius {} in meters.", valueToConvert);
357 return valueToConvert;
361 * Update basic channels: batteryLevel, lastLocation, lastReport
363 * @param message Received message.
365 private void updateBaseChannels(LocationMessage message, String... channels) {
366 logger.debug("Update base channels for tracker {} from message: {}", trackerId, message);
368 for (String channel : channels) {
370 case CHANNEL_LAST_REPORT:
371 State timestamp = message.getTimestamp();
372 updateState(CHANNEL_LAST_REPORT, timestamp);
373 logger.trace("{} -> {}", CHANNEL_LAST_REPORT, timestamp);
375 case CHANNEL_LAST_LOCATION:
376 State newLocation = message.getTrackerLocation();
377 updateState(CHANNEL_LAST_LOCATION, newLocation);
378 logger.trace("{} -> {}", CHANNEL_LAST_LOCATION, newLocation);
380 case CHANNEL_BATTERY_LEVEL:
381 State batteryLevel = message.getBatteryLevel();
382 updateState(CHANNEL_BATTERY_LEVEL, batteryLevel);
383 logger.trace("{} -> {}", CHANNEL_BATTERY_LEVEL, batteryLevel);
385 case CHANNEL_GPS_ACCURACY:
386 State accuracy = message.getGpsAccuracy();
387 updateState(CHANNEL_GPS_ACCURACY, accuracy);
388 logger.trace("{} -> {}", CHANNEL_GPS_ACCURACY, accuracy);
395 * Location message handling.
397 * @param lm Location message
399 public void updateLocation(LocationMessage lm) {
400 this.lastMessage = lm;
401 updateStatus(ThingStatus.ONLINE);
402 updateChannelsWithLocation(lm);
403 notificationBroker.sendNotification(lm);
407 * Transition message handling
409 * @param tm Transition message
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);
418 updateChannelsWithLocation(tm);
419 updateTriggerChannelsWithTransition(tm);
421 notificationBroker.sendNotification(tm);
425 * Get notification to return to the tracker (supported by OwnTracks only)
427 * @return List of notifications received from other trackers
429 public List<LocationMessage> getNotifications() {
430 return notificationHandler.getNotifications();