2 * Copyright (c) 2010-2020 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
69 public abstract class AVMFritzBaseBridgeHandler extends BaseBridgeHandler {
71 private final Logger logger = LoggerFactory.getLogger(AVMFritzBaseBridgeHandler.class);
74 * Initial delay in s for polling job.
76 private static final int INITIAL_DELAY = 1;
79 * Refresh interval which is used to poll values from the FRITZ!Box web interface (optional, defaults to 15 s)
81 private long refreshInterval = 15;
84 * Interface object for querying the FRITZ!Box web interface
86 protected @Nullable FritzAhaWebInterface connection;
89 * Schedule for polling
91 private @Nullable ScheduledFuture<?> pollingJob;
94 * Shared instance of HTTP client for asynchronous calls
96 protected final HttpClient httpClient;
98 private final AVMFritzDynamicCommandDescriptionProvider commandDescriptionProvider;
100 protected final List<FritzAhaStatusListener> listeners = new CopyOnWriteArrayList<>();
103 * keeps track of the {@link ChannelUID} for the 'apply_template' {@link Channel}
105 private final ChannelUID applyTemplateChannelUID;
110 * @param bridge Bridge object representing a FRITZ!Box
112 public AVMFritzBaseBridgeHandler(Bridge bridge, HttpClient httpClient,
113 AVMFritzDynamicCommandDescriptionProvider commandDescriptionProvider) {
115 this.httpClient = httpClient;
116 this.commandDescriptionProvider = commandDescriptionProvider;
118 applyTemplateChannelUID = new ChannelUID(bridge.getUID(), CHANNEL_APPLY_TEMPLATE);
122 public void initialize() {
123 boolean configValid = true;
125 AVMFritzBoxConfiguration config = getConfigAs(AVMFritzBoxConfiguration.class);
127 String localIpAddress = config.ipAddress;
128 if (localIpAddress == null || localIpAddress.isBlank()) {
129 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
130 "The 'ipAddress' parameter must be configured.");
133 refreshInterval = config.pollingInterval;
134 if (refreshInterval < 5) {
135 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
136 "The 'pollingInterval' parameter must be greater then at least 5 seconds.");
141 updateStatus(ThingStatus.UNKNOWN);
146 protected synchronized void manageConnections() {
147 AVMFritzBoxConfiguration config = getConfigAs(AVMFritzBoxConfiguration.class);
148 if (this.connection == null) {
149 this.connection = new FritzAhaWebInterface(config, this, httpClient);
156 public void channelLinked(ChannelUID channelUID) {
158 super.channelLinked(channelUID);
162 public void channelUnlinked(ChannelUID channelUID) {
164 super.channelUnlinked(channelUID);
168 public void dispose() {
173 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
174 if (childHandler instanceof FritzAhaStatusListener) {
175 registerStatusListener((FritzAhaStatusListener) childHandler);
180 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
181 if (childHandler instanceof FritzAhaStatusListener) {
182 unregisterStatusListener((FritzAhaStatusListener) childHandler);
187 public Collection<Class<? extends ThingHandlerService>> getServices() {
188 return Collections.singleton(AVMFritzDiscoveryService.class);
191 public boolean registerStatusListener(FritzAhaStatusListener listener) {
192 return listeners.add(listener);
195 public boolean unregisterStatusListener(FritzAhaStatusListener listener) {
196 return listeners.remove(listener);
202 protected void startPolling() {
203 ScheduledFuture<?> localPollingJob = pollingJob;
204 if (localPollingJob == null || localPollingJob.isCancelled()) {
205 logger.debug("Start polling job at interval {}s", refreshInterval);
206 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, INITIAL_DELAY, refreshInterval, TimeUnit.SECONDS);
213 protected void stopPolling() {
214 ScheduledFuture<?> localPollingJob = pollingJob;
215 if (localPollingJob != null && !localPollingJob.isCancelled()) {
216 logger.debug("Stop polling job");
217 localPollingJob.cancel(true);
225 private void poll() {
226 FritzAhaWebInterface webInterface = getWebInterface();
227 if (webInterface != null) {
228 logger.debug("Poll FRITZ!Box for updates {}", thing.getUID());
229 FritzAhaUpdateCallback updateCallback = new FritzAhaUpdateCallback(webInterface, this);
230 webInterface.asyncGet(updateCallback);
231 if (isLinked(applyTemplateChannelUID)) {
232 logger.debug("Poll FRITZ!Box for templates {}", thing.getUID());
233 FritzAhaUpdateTemplatesCallback templateCallback = new FritzAhaUpdateTemplatesCallback(webInterface,
235 webInterface.asyncGet(templateCallback);
241 * Called from {@link FritzAhaWebInterface#authenticate()} to update the bridge status because updateStatus is
244 * @param status Bridge status
245 * @param statusDetail Bridge status detail
246 * @param description Bridge status description
248 public void setStatusInfo(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
249 updateStatus(status, statusDetail, description);
253 * Called from {@link FritzAhaApplyTemplateCallback} to provide new templates for things.
255 * @param templateList list of template models
257 public void addTemplateList(List<TemplateModel> templateList) {
258 commandDescriptionProvider.setCommandOptions(applyTemplateChannelUID,
259 templateList.stream().map(TemplateModel::toCommandOption).collect(Collectors.toList()));
263 * Called from {@link FritzAhaUpdateCallback} to provide new devices.
265 * @param deviceList list of devices
267 public void onDeviceListAdded(List<AVMFritzBaseModel> deviceList) {
268 final Map<String, AVMFritzBaseModel> deviceIdentifierMap = deviceList.stream()
269 .collect(Collectors.toMap(it -> it.getIdentifier(), Function.identity()));
270 getThing().getThings().forEach(childThing -> {
271 final AVMFritzBaseThingHandler childHandler = (AVMFritzBaseThingHandler) childThing.getHandler();
272 if (childHandler != null) {
273 final AVMFritzBaseModel device = deviceIdentifierMap.get(childHandler.getIdentifier());
274 if (device != null) {
275 deviceList.remove(device);
276 listeners.forEach(listener -> listener.onDeviceUpdated(childThing.getUID(), device));
278 listeners.forEach(listener -> listener.onDeviceGone(childThing.getUID()));
281 logger.debug("Handler missing for thing '{}'", childThing.getUID());
284 deviceList.forEach(device -> {
285 listeners.forEach(listener -> listener.onDeviceAdded(device));
290 * Builds a {@link ThingUID} from a device model. The UID is build from the
291 * {@link AVMFritzBindingConstants#BINDING_ID} and
292 * value of {@link AVMFritzBaseModel#getProductName()} in which all characters NOT matching the RegEx [^a-zA-Z0-9_]
293 * are replaced by "_".
295 * @param device Discovered device model
296 * @return ThingUID without illegal characters.
298 public @Nullable ThingUID getThingUID(AVMFritzBaseModel device) {
299 ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, getThingTypeId(device));
300 ThingUID bridgeUID = thing.getUID();
301 String thingName = getThingName(device);
303 if (SUPPORTED_BUTTON_THING_TYPES_UIDS.contains(thingTypeUID)
304 || SUPPORTED_HEATING_THING_TYPES.contains(thingTypeUID)
305 || SUPPORTED_DEVICE_THING_TYPES_UIDS.contains(thingTypeUID)) {
306 return new ThingUID(thingTypeUID, bridgeUID, thingName);
307 } else if (device.isHeatingThermostat()) {
308 return new ThingUID(GROUP_HEATING_THING_TYPE, bridgeUID, thingName);
309 } else if (device.isSwitchableOutlet()) {
310 return new ThingUID(GROUP_SWITCH_THING_TYPE, bridgeUID, thingName);
318 * @param device Discovered device model
319 * @return ThingTypeId without illegal characters.
321 public String getThingTypeId(AVMFritzBaseModel device) {
322 if (device instanceof GroupModel) {
323 if (device.isHeatingThermostat()) {
324 return GROUP_HEATING;
325 } else if (device.isSwitchableOutlet()) {
328 } else if (device instanceof DeviceModel && device.isHANFUNUnit()) {
329 List<String> interfaces = Arrays
330 .asList(((DeviceModel) device).getEtsiunitinfo().getInterfaces().split(","));
331 if (interfaces.contains(HAN_FUN_INTERFACE_ALERT)) {
332 return DEVICE_HAN_FUN_CONTACT;
333 } else if (interfaces.contains(HAN_FUN_INTERFACE_SIMPLE_BUTTON)) {
334 return DEVICE_HAN_FUN_SWITCH;
337 return device.getProductName().replaceAll(INVALID_PATTERN, "_");
342 * @param device Discovered device model
343 * @return Thing name without illegal characters.
345 public String getThingName(AVMFritzBaseModel device) {
346 return device.getIdentifier().replaceAll(INVALID_PATTERN, "_");
350 public void handleCommand(ChannelUID channelUID, Command command) {
351 String channelId = channelUID.getIdWithoutGroup();
352 logger.debug("Handle command '{}' for channel {}", command, channelId);
353 if (command == RefreshType.REFRESH) {
354 handleRefreshCommand();
357 FritzAhaWebInterface fritzBox = getWebInterface();
358 if (fritzBox == null) {
359 logger.debug("Cannot handle command '{}' because connection is missing", command);
362 if (CHANNEL_APPLY_TEMPLATE.equals(channelId)) {
363 if (command instanceof StringType) {
364 fritzBox.applyTemplate(command.toString());
367 logger.debug("Received unknown channel {}", channelId);
372 * Provides the web interface object.
374 * @return The web interface object
376 public @Nullable FritzAhaWebInterface getWebInterface() {
381 * Handles a refresh command.
383 public void handleRefreshCommand() {
384 scheduler.submit(this::poll);