]> git.basschouten.com Git - openhab-addons.git/blob
01106f3c53608b6771f8611e6482960c05b0201c
[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.avmfritz.internal.handler;
14
15 import static org.openhab.binding.avmfritz.internal.AVMFritzBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.function.Predicate;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.client.HttpClient;
25 import org.openhab.binding.avmfritz.internal.AVMFritzBindingConstants;
26 import org.openhab.binding.avmfritz.internal.AVMFritzDynamicCommandDescriptionProvider;
27 import org.openhab.binding.avmfritz.internal.config.AVMFritzBoxConfiguration;
28 import org.openhab.binding.avmfritz.internal.config.AVMFritzDeviceConfiguration;
29 import org.openhab.binding.avmfritz.internal.dto.AVMFritzBaseModel;
30 import org.openhab.binding.avmfritz.internal.dto.PowerMeterModel;
31 import org.openhab.binding.avmfritz.internal.dto.SwitchModel;
32 import org.openhab.binding.avmfritz.internal.hardware.FritzAhaStatusListener;
33 import org.openhab.binding.avmfritz.internal.hardware.FritzAhaWebInterface;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.OpenClosedType;
36 import org.openhab.core.library.types.QuantityType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.library.unit.Units;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.Channel;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.ThingTypeUID;
47 import org.openhab.core.thing.ThingUID;
48 import org.openhab.core.thing.binding.ThingHandlerCallback;
49 import org.openhab.core.thing.type.ChannelTypeUID;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.openhab.core.types.State;
53 import org.openhab.core.types.UnDefType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 /**
58  * Handler for a FRITZ!Powerline 546E device. Handles polling of values from AHA devices and commands, which are sent to
59  * one of the channels.
60  *
61  * @author Robert Bausdorf - Initial contribution
62  * @author Christoph Weitkamp - Added support for groups
63  */
64 @NonNullByDefault
65 public class Powerline546EHandler extends AVMFritzBaseBridgeHandler implements FritzAhaStatusListener {
66
67     private final Logger logger = LoggerFactory.getLogger(Powerline546EHandler.class);
68
69     private @Nullable String identifier;
70
71     /**
72      * Constructor
73      *
74      * @param bridge Bridge object representing a FRITZ!Powerline 546E
75      */
76     public Powerline546EHandler(Bridge bridge, HttpClient httpClient,
77             AVMFritzDynamicCommandDescriptionProvider commandDescriptionProvider) {
78         super(bridge, httpClient, commandDescriptionProvider);
79     }
80
81     @Override
82     public void initialize() {
83         final AVMFritzDeviceConfiguration config = getConfigAs(AVMFritzDeviceConfiguration.class);
84         final String newIdentifier = config.ain;
85         if (newIdentifier != null && !newIdentifier.isBlank()) {
86             this.identifier = newIdentifier;
87         }
88
89         super.initialize();
90     }
91
92     @SuppressWarnings({ "null", "unused" })
93     @Override
94     public void onDeviceListAdded(List<AVMFritzBaseModel> devicelist) {
95         final String ain = getIdentifier();
96         final Predicate<AVMFritzBaseModel> predicate = ain == null ? it -> thing.getUID().equals(getThingUID(it))
97                 : it -> ain.equals(it.getIdentifier());
98         final AVMFritzBaseModel device = devicelist.stream().filter(predicate).findFirst().orElse(null);
99         if (device != null) {
100             devicelist.remove(device);
101             onDeviceUpdated(thing.getUID(), device);
102         } else {
103             onDeviceGone(thing.getUID());
104         }
105         super.onDeviceListAdded(devicelist);
106     }
107
108     @Override
109     public void onDeviceAdded(AVMFritzBaseModel device) {
110         // nothing to do
111     }
112
113     @Override
114     public void onDeviceUpdated(ThingUID thingUID, AVMFritzBaseModel device) {
115         if (thing.getUID().equals(thingUID)) {
116             // save AIN to config for FRITZ!Powerline 546E stand-alone
117             if (this.identifier == null) {
118                 this.identifier = device.getIdentifier();
119             }
120
121             logger.debug("Update self '{}' with device model: {}", thingUID, device);
122             if (device.getPresent() == 1) {
123                 updateStatus(ThingStatus.ONLINE);
124             } else {
125                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device not present");
126             }
127
128             updateProperties(device);
129
130             if (device.isPowermeter()) {
131                 updatePowermeter(device.getPowermeter());
132             }
133             if (device.isSwitchableOutlet()) {
134                 updateSwitchableOutlet(device.getSwitch());
135             }
136         }
137     }
138
139     private void updateSwitchableOutlet(@Nullable SwitchModel switchModel) {
140         if (switchModel != null) {
141             updateThingChannelState(CHANNEL_MODE, new StringType(switchModel.getMode()));
142             updateThingChannelState(CHANNEL_LOCKED,
143                     BigDecimal.ZERO.equals(switchModel.getLock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
144             updateThingChannelState(CHANNEL_DEVICE_LOCKED,
145                     BigDecimal.ZERO.equals(switchModel.getDevicelock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
146             BigDecimal state = switchModel.getState();
147             if (state == null) {
148                 updateThingChannelState(CHANNEL_OUTLET, UnDefType.UNDEF);
149             } else {
150                 updateThingChannelState(CHANNEL_OUTLET, SwitchModel.ON.equals(state) ? OnOffType.ON : OnOffType.OFF);
151             }
152         }
153     }
154
155     private void updatePowermeter(@Nullable PowerMeterModel powerMeterModel) {
156         if (powerMeterModel != null) {
157             updateThingChannelState(CHANNEL_ENERGY, new QuantityType<>(powerMeterModel.getEnergy(), Units.WATT_HOUR));
158             updateThingChannelState(CHANNEL_POWER, new QuantityType<>(powerMeterModel.getPower(), Units.WATT));
159             updateThingChannelState(CHANNEL_VOLTAGE, new QuantityType<>(powerMeterModel.getVoltage(), Units.VOLT));
160         }
161     }
162
163     /**
164      * Updates thing properties.
165      *
166      * @param device the {@link AVMFritzBaseModel}
167      */
168     private void updateProperties(AVMFritzBaseModel device) {
169         Map<String, String> editProperties = editProperties();
170         editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
171         updateProperties(editProperties);
172     }
173
174     /**
175      * Updates thing channels and creates dynamic channels if missing.
176      *
177      * @param channelId ID of the channel to be updated.
178      * @param state State to be set.
179      */
180     private void updateThingChannelState(String channelId, State state) {
181         Channel channel = thing.getChannel(channelId);
182         if (channel != null) {
183             updateState(channel.getUID(), state);
184         } else {
185             logger.debug("Channel '{}' in thing '{}' does not exist, recreating thing.", channelId, thing.getUID());
186             createChannel(channelId);
187         }
188     }
189
190     /**
191      * Creates a {@link ChannelTypeUID} from the given channel id.
192      *
193      * @param channelId ID of the channel type UID to be created.
194      * @return the channel type UID
195      */
196     private ChannelTypeUID createChannelTypeUID(String channelId) {
197         final ChannelTypeUID channelTypeUID;
198         switch (channelId) {
199             case CHANNEL_BATTERY:
200                 channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_BATTERY_LEVEL.getUID();
201                 break;
202             case CHANNEL_VOLTAGE:
203                 channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_ELECTRIC_VOLTAGE.getUID();
204                 break;
205             default:
206                 channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
207                 break;
208         }
209         return channelTypeUID;
210     }
211
212     /**
213      * Creates new channels for the thing.
214      *
215      * @param channelId ID of the channel to be created.
216      */
217     private void createChannel(String channelId) {
218         ThingHandlerCallback callback = getCallback();
219         if (callback != null) {
220             final ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
221             final ChannelTypeUID channelTypeUID = createChannelTypeUID(channelId);
222             final Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).build();
223             updateThing(editThing().withoutChannel(channelUID).withChannel(channel).build());
224         }
225     }
226
227     @Override
228     public void onDeviceGone(ThingUID thingUID) {
229         if (thing.getUID().equals(thingUID)) {
230             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Device not present in response");
231         }
232     }
233
234     /**
235      * Builds a {@link ThingUID} from a device model. The UID is build from the
236      * {@link AVMFritzBindingConstants#BINDING_ID} and value of
237      * {@link AVMFritzBaseModel#getProductName()} in which all characters NOT matching
238      * the regex [^a-zA-Z0-9_] are replaced by "_".
239      *
240      * @param device Discovered device model
241      * @return ThingUID without illegal characters.
242      */
243     @Override
244     public @Nullable ThingUID getThingUID(AVMFritzBaseModel device) {
245         ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, getThingTypeId(device).concat("_Solo"));
246         String ipAddress = getConfigAs(AVMFritzBoxConfiguration.class).ipAddress;
247
248         if (POWERLINE546E_STANDALONE_THING_TYPE.equals(thingTypeUID)) {
249             String thingName = "fritz.powerline".equals(ipAddress) ? ipAddress
250                     : ipAddress.replaceAll(INVALID_PATTERN, "_");
251             return new ThingUID(thingTypeUID, thingName);
252         } else {
253             return super.getThingUID(device);
254         }
255     }
256
257     @Override
258     public void handleCommand(ChannelUID channelUID, Command command) {
259         String channelId = channelUID.getIdWithoutGroup();
260         logger.debug("Handle command '{}' for channel {}", command, channelId);
261         if (command == RefreshType.REFRESH) {
262             handleRefreshCommand();
263             return;
264         }
265         FritzAhaWebInterface fritzBox = getWebInterface();
266         if (fritzBox == null) {
267             logger.debug("Cannot handle command '{}' because connection is missing", command);
268             return;
269         }
270         String ain = getIdentifier();
271         if (ain == null) {
272             logger.debug("Cannot handle command '{}' because AIN is missing", command);
273             return;
274         }
275         switch (channelId) {
276             case CHANNEL_MODE:
277             case CHANNEL_LOCKED:
278             case CHANNEL_DEVICE_LOCKED:
279             case CHANNEL_ENERGY:
280             case CHANNEL_POWER:
281             case CHANNEL_VOLTAGE:
282                 logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command);
283                 break;
284             case CHANNEL_APPLY_TEMPLATE:
285                 if (command instanceof StringType) {
286                     fritzBox.applyTemplate(command.toString());
287                 }
288                 break;
289             case CHANNEL_OUTLET:
290                 if (command instanceof OnOffType) {
291                     fritzBox.setSwitch(ain, OnOffType.ON.equals(command));
292                 }
293                 break;
294             default:
295                 super.handleCommand(channelUID, command);
296                 break;
297         }
298     }
299
300     /**
301      * Returns the AIN.
302      *
303      * @return the AIN
304      */
305     public @Nullable String getIdentifier() {
306         return identifier;
307     }
308 }