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.avmfritz.internal.handler;
15 import static org.openhab.binding.avmfritz.internal.AVMFritzBindingConstants.*;
16 import static org.openhab.binding.avmfritz.internal.dto.DeviceModel.ETSUnitInfoModel.*;
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.List;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.function.Function;
27 import java.util.stream.Collectors;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.openhab.binding.avmfritz.internal.AVMFritzBindingConstants;
33 import org.openhab.binding.avmfritz.internal.AVMFritzDynamicCommandDescriptionProvider;
34 import org.openhab.binding.avmfritz.internal.config.AVMFritzBoxConfiguration;
35 import org.openhab.binding.avmfritz.internal.discovery.AVMFritzDiscoveryService;
36 import org.openhab.binding.avmfritz.internal.dto.AVMFritzBaseModel;
37 import org.openhab.binding.avmfritz.internal.dto.DeviceModel;
38 import org.openhab.binding.avmfritz.internal.dto.GroupModel;
39 import org.openhab.binding.avmfritz.internal.dto.templates.TemplateModel;
40 import org.openhab.binding.avmfritz.internal.hardware.FritzAhaStatusListener;
41 import org.openhab.binding.avmfritz.internal.hardware.FritzAhaWebInterface;
42 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaApplyTemplateCallback;
43 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaUpdateCallback;
44 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaUpdateTemplatesCallback;
45 import org.openhab.core.library.types.StringType;
46 import org.openhab.core.thing.Bridge;
47 import org.openhab.core.thing.ChannelUID;
48 import org.openhab.core.thing.Thing;
49 import org.openhab.core.thing.ThingStatus;
50 import org.openhab.core.thing.ThingStatusDetail;
51 import org.openhab.core.thing.ThingTypeUID;
52 import org.openhab.core.thing.ThingUID;
53 import org.openhab.core.thing.binding.BaseBridgeHandler;
54 import org.openhab.core.thing.binding.ThingHandler;
55 import org.openhab.core.thing.binding.ThingHandlerService;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
62 * Abstract handler for a FRITZ! bridge. Handles polling of values from AHA devices.
64 * @author Robert Bausdorf - Initial contribution
65 * @author Christoph Weitkamp - Added support for AVM FRITZ!DECT 300 and Comet DECT
66 * @author Christoph Weitkamp - Added support for groups
67 * @author Ulrich Mertin - Added support for HAN-FUN blinds
70 public abstract class AVMFritzBaseBridgeHandler extends BaseBridgeHandler {
72 private final Logger logger = LoggerFactory.getLogger(AVMFritzBaseBridgeHandler.class);
75 * Initial delay in s for polling job.
77 private static final int INITIAL_DELAY = 1;
80 * Refresh interval which is used to poll values from the FRITZ!Box web interface (optional, defaults to 15 s)
82 private long refreshInterval = 15;
85 * Interface object for querying the FRITZ!Box web interface
87 protected @Nullable FritzAhaWebInterface connection;
90 * Schedule for polling
92 private @Nullable ScheduledFuture<?> pollingJob;
95 * Shared instance of HTTP client for asynchronous calls
97 protected final HttpClient httpClient;
99 private final AVMFritzDynamicCommandDescriptionProvider commandDescriptionProvider;
101 protected final List<FritzAhaStatusListener> listeners = new CopyOnWriteArrayList<>();
104 * keeps track of the {@link ChannelUID} for the 'apply_template' {@link Channel}
106 private final ChannelUID applyTemplateChannelUID;
111 * @param bridge Bridge object representing a FRITZ!Box
113 public AVMFritzBaseBridgeHandler(Bridge bridge, HttpClient httpClient,
114 AVMFritzDynamicCommandDescriptionProvider commandDescriptionProvider) {
116 this.httpClient = httpClient;
117 this.commandDescriptionProvider = commandDescriptionProvider;
119 applyTemplateChannelUID = new ChannelUID(bridge.getUID(), CHANNEL_APPLY_TEMPLATE);
123 public void initialize() {
124 boolean configValid = true;
126 AVMFritzBoxConfiguration config = getConfigAs(AVMFritzBoxConfiguration.class);
128 String localIpAddress = config.ipAddress;
129 if (localIpAddress == null || localIpAddress.isBlank()) {
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
131 "The 'ipAddress' parameter must be configured.");
134 refreshInterval = config.pollingInterval;
135 if (refreshInterval < 5) {
136 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
137 "The 'pollingInterval' parameter must be greater then at least 5 seconds.");
142 updateStatus(ThingStatus.UNKNOWN);
147 protected synchronized void manageConnections() {
148 AVMFritzBoxConfiguration config = getConfigAs(AVMFritzBoxConfiguration.class);
149 if (this.connection == null) {
150 this.connection = new FritzAhaWebInterface(config, this, httpClient);
157 public void channelLinked(ChannelUID channelUID) {
159 super.channelLinked(channelUID);
163 public void channelUnlinked(ChannelUID channelUID) {
165 super.channelUnlinked(channelUID);
169 public void dispose() {
174 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
175 if (childHandler instanceof FritzAhaStatusListener) {
176 registerStatusListener((FritzAhaStatusListener) childHandler);
181 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
182 if (childHandler instanceof FritzAhaStatusListener) {
183 unregisterStatusListener((FritzAhaStatusListener) childHandler);
188 public Collection<Class<? extends ThingHandlerService>> getServices() {
189 return Collections.singleton(AVMFritzDiscoveryService.class);
192 public boolean registerStatusListener(FritzAhaStatusListener listener) {
193 return listeners.add(listener);
196 public boolean unregisterStatusListener(FritzAhaStatusListener listener) {
197 return listeners.remove(listener);
203 protected void startPolling() {
204 ScheduledFuture<?> localPollingJob = pollingJob;
205 if (localPollingJob == null || localPollingJob.isCancelled()) {
206 logger.debug("Start polling job at interval {}s", refreshInterval);
207 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, INITIAL_DELAY, refreshInterval, TimeUnit.SECONDS);
214 protected void stopPolling() {
215 ScheduledFuture<?> localPollingJob = pollingJob;
216 if (localPollingJob != null && !localPollingJob.isCancelled()) {
217 logger.debug("Stop polling job");
218 localPollingJob.cancel(true);
226 private void poll() {
227 FritzAhaWebInterface webInterface = getWebInterface();
228 if (webInterface != null) {
229 logger.debug("Poll FRITZ!Box for updates {}", thing.getUID());
230 FritzAhaUpdateCallback updateCallback = new FritzAhaUpdateCallback(webInterface, this);
231 webInterface.asyncGet(updateCallback);
232 if (isLinked(applyTemplateChannelUID)) {
233 logger.debug("Poll FRITZ!Box for templates {}", thing.getUID());
234 FritzAhaUpdateTemplatesCallback templateCallback = new FritzAhaUpdateTemplatesCallback(webInterface,
236 webInterface.asyncGet(templateCallback);
242 * Called from {@link FritzAhaWebInterface#authenticate()} to update the bridge status because updateStatus is
245 * @param status Bridge status
246 * @param statusDetail Bridge status detail
247 * @param description Bridge status description
249 public void setStatusInfo(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
250 updateStatus(status, statusDetail, description);
254 * Called from {@link FritzAhaApplyTemplateCallback} to provide new templates for things.
256 * @param templateList list of template models
258 public void addTemplateList(List<TemplateModel> templateList) {
259 commandDescriptionProvider.setCommandOptions(applyTemplateChannelUID,
260 templateList.stream().map(TemplateModel::toCommandOption).collect(Collectors.toList()));
264 * Called from {@link FritzAhaUpdateCallback} to provide new devices.
266 * @param deviceList list of devices
268 public void onDeviceListAdded(List<AVMFritzBaseModel> deviceList) {
269 final Map<String, AVMFritzBaseModel> deviceIdentifierMap = deviceList.stream()
270 .collect(Collectors.toMap(it -> it.getIdentifier(), Function.identity()));
271 getThing().getThings().forEach(childThing -> {
272 final AVMFritzBaseThingHandler childHandler = (AVMFritzBaseThingHandler) childThing.getHandler();
273 if (childHandler != null) {
274 final AVMFritzBaseModel device = deviceIdentifierMap.get(childHandler.getIdentifier());
275 if (device != null) {
276 deviceList.remove(device);
277 listeners.forEach(listener -> listener.onDeviceUpdated(childThing.getUID(), device));
279 listeners.forEach(listener -> listener.onDeviceGone(childThing.getUID()));
282 logger.debug("Handler missing for thing '{}'", childThing.getUID());
285 deviceList.forEach(device -> {
286 listeners.forEach(listener -> listener.onDeviceAdded(device));
291 * Builds a {@link ThingUID} from a device model. The UID is build from the
292 * {@link AVMFritzBindingConstants#BINDING_ID} and
293 * value of {@link AVMFritzBaseModel#getProductName()} in which all characters NOT matching the RegEx [^a-zA-Z0-9_]
294 * are replaced by "_".
296 * @param device Discovered device model
297 * @return ThingUID without illegal characters.
299 public @Nullable ThingUID getThingUID(AVMFritzBaseModel device) {
300 String id = getThingTypeId(device);
301 ThingTypeUID thingTypeUID = id.isEmpty() ? null : new ThingTypeUID(BINDING_ID, id);
302 ThingUID bridgeUID = thing.getUID();
303 String thingName = getThingName(device);
305 if (thingTypeUID != null && (SUPPORTED_BUTTON_THING_TYPES_UIDS.contains(thingTypeUID)
306 || SUPPORTED_HEATING_THING_TYPES.contains(thingTypeUID)
307 || SUPPORTED_DEVICE_THING_TYPES_UIDS.contains(thingTypeUID))) {
308 return new ThingUID(thingTypeUID, bridgeUID, thingName);
309 } else if (device.isHeatingThermostat()) {
310 return new ThingUID(GROUP_HEATING_THING_TYPE, bridgeUID, thingName);
311 } else if (device.isSwitchableOutlet()) {
312 return new ThingUID(GROUP_SWITCH_THING_TYPE, bridgeUID, thingName);
320 * @param device Discovered device model
321 * @return ThingTypeId without illegal characters.
323 public String getThingTypeId(AVMFritzBaseModel device) {
324 if (device instanceof GroupModel) {
325 if (device.isHeatingThermostat()) {
326 return GROUP_HEATING;
327 } else if (device.isSwitchableOutlet()) {
330 } else if (device instanceof DeviceModel && device.isHANFUNUnit()) {
331 if (device.isHANFUNBlinds()) {
332 return DEVICE_HAN_FUN_BLINDS;
333 } else if (device.isColorLight()) {
334 return DEVICE_HAN_FUN_COLOR_BULB;
335 } else if (device.isDimmableLight()) {
336 return DEVICE_HAN_FUN_DIMMABLE_BULB;
338 List<String> interfaces = Arrays
339 .asList(((DeviceModel) device).getEtsiunitinfo().getInterfaces().split(","));
340 if (interfaces.contains(HAN_FUN_INTERFACE_ALERT)) {
341 return DEVICE_HAN_FUN_CONTACT;
342 } else if (interfaces.contains(HAN_FUN_INTERFACE_SIMPLE_BUTTON)) {
343 return DEVICE_HAN_FUN_SWITCH;
344 } else if (interfaces.contains(HAN_FUN_INTERFACE_ON_OFF)) {
345 return DEVICE_HAN_FUN_ON_OFF;
348 return device.getProductName().replaceAll(INVALID_PATTERN, "_");
353 * @param device Discovered device model
354 * @return Thing name without illegal characters.
356 public String getThingName(AVMFritzBaseModel device) {
357 return device.getIdentifier().replaceAll(INVALID_PATTERN, "_");
361 public void handleCommand(ChannelUID channelUID, Command command) {
362 String channelId = channelUID.getIdWithoutGroup();
363 logger.debug("Handle command '{}' for channel {}", command, channelId);
364 if (command == RefreshType.REFRESH) {
365 handleRefreshCommand();
368 FritzAhaWebInterface fritzBox = getWebInterface();
369 if (fritzBox == null) {
370 logger.debug("Cannot handle command '{}' because connection is missing", command);
373 if (CHANNEL_APPLY_TEMPLATE.equals(channelId)) {
374 if (command instanceof StringType) {
375 fritzBox.applyTemplate(command.toString());
378 logger.debug("Received unknown channel {}", channelId);
383 * Provides the web interface object.
385 * @return The web interface object
387 public @Nullable FritzAhaWebInterface getWebInterface() {
392 * Handles a refresh command.
394 public void handleRefreshCommand() {
395 scheduler.submit(this::poll);