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.*;
17 import java.math.BigDecimal;
18 import java.util.List;
20 import java.util.function.Predicate;
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;
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.
61 * @author Robert Bausdorf - Initial contribution
62 * @author Christoph Weitkamp - Added support for groups
65 public class Powerline546EHandler extends AVMFritzBaseBridgeHandler implements FritzAhaStatusListener {
67 private final Logger logger = LoggerFactory.getLogger(Powerline546EHandler.class);
70 * keeps track of the current state for handling of increase/decrease
72 private @Nullable AVMFritzBaseModel state;
73 private @Nullable String identifier;
78 * @param bridge Bridge object representing a FRITZ!Powerline 546E
80 public Powerline546EHandler(Bridge bridge, HttpClient httpClient,
81 AVMFritzDynamicCommandDescriptionProvider commandDescriptionProvider) {
82 super(bridge, httpClient, commandDescriptionProvider);
86 public void initialize() {
87 final AVMFritzDeviceConfiguration config = getConfigAs(AVMFritzDeviceConfiguration.class);
88 final String newIdentifier = config.ain;
89 if (newIdentifier != null && !newIdentifier.isBlank()) {
90 this.identifier = newIdentifier;
96 @SuppressWarnings({ "null", "unused" })
98 public void onDeviceListAdded(List<AVMFritzBaseModel> devicelist) {
99 final String ain = getIdentifier();
100 final Predicate<AVMFritzBaseModel> predicate = ain == null ? it -> thing.getUID().equals(getThingUID(it))
101 : it -> ain.equals(it.getIdentifier());
102 final AVMFritzBaseModel device = devicelist.stream().filter(predicate).findFirst().orElse(null);
103 if (device != null) {
104 devicelist.remove(device);
105 onDeviceUpdated(thing.getUID(), device);
107 onDeviceGone(thing.getUID());
109 super.onDeviceListAdded(devicelist);
113 public void onDeviceAdded(AVMFritzBaseModel device) {
118 public void onDeviceUpdated(ThingUID thingUID, AVMFritzBaseModel device) {
119 if (thing.getUID().equals(thingUID)) {
120 // save AIN to config for FRITZ!Powerline 546E stand-alone
121 if (this.identifier == null) {
122 this.identifier = device.getIdentifier();
125 logger.debug("Update self '{}' with device model: {}", thingUID, device);
126 if (device.getPresent() == 1) {
127 updateStatus(ThingStatus.ONLINE);
129 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device not present");
133 updateProperties(device);
135 if (device.isPowermeter()) {
136 updatePowermeter(device.getPowermeter());
138 if (device.isSwitchableOutlet()) {
139 updateSwitchableOutlet(device.getSwitch());
144 private void updateSwitchableOutlet(@Nullable SwitchModel switchModel) {
145 if (switchModel != null) {
146 updateThingChannelState(CHANNEL_MODE, new StringType(switchModel.getMode()));
147 updateThingChannelState(CHANNEL_LOCKED,
148 BigDecimal.ZERO.equals(switchModel.getLock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
149 updateThingChannelState(CHANNEL_DEVICE_LOCKED,
150 BigDecimal.ZERO.equals(switchModel.getDevicelock()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
151 BigDecimal state = switchModel.getState();
153 updateThingChannelState(CHANNEL_OUTLET, UnDefType.UNDEF);
155 updateThingChannelState(CHANNEL_OUTLET, SwitchModel.ON.equals(state) ? OnOffType.ON : OnOffType.OFF);
160 private void updatePowermeter(@Nullable PowerMeterModel powerMeterModel) {
161 if (powerMeterModel != null) {
162 updateThingChannelState(CHANNEL_ENERGY, new QuantityType<>(powerMeterModel.getEnergy(), Units.WATT_HOUR));
163 updateThingChannelState(CHANNEL_POWER, new QuantityType<>(powerMeterModel.getPower(), Units.WATT));
164 updateThingChannelState(CHANNEL_VOLTAGE, new QuantityType<>(powerMeterModel.getVoltage(), Units.VOLT));
169 * Updates thing properties.
171 * @param device the {@link AVMFritzBaseModel}
173 private void updateProperties(AVMFritzBaseModel device) {
174 Map<String, String> editProperties = editProperties();
175 editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
176 updateProperties(editProperties);
180 * Updates thing channels and creates dynamic channels if missing.
182 * @param channelId ID of the channel to be updated.
183 * @param state State to be set.
185 private void updateThingChannelState(String channelId, State state) {
186 Channel channel = thing.getChannel(channelId);
187 if (channel != null) {
188 updateState(channel.getUID(), state);
190 logger.debug("Channel '{}' in thing '{}' does not exist, recreating thing.", channelId, thing.getUID());
191 createChannel(channelId);
196 * Creates new channels for the thing.
198 * @param channelId ID of the channel to be created.
200 private void createChannel(String channelId) {
201 ThingHandlerCallback callback = getCallback();
202 if (callback != null) {
203 ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
204 ChannelTypeUID channelTypeUID = CHANNEL_BATTERY.equals(channelId)
205 ? DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_BATTERY_LEVEL.getUID()
206 : new ChannelTypeUID(BINDING_ID, channelId);
207 Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).build();
208 updateThing(editThing().withoutChannel(channelUID).withChannel(channel).build());
213 public void onDeviceGone(ThingUID thingUID) {
214 if (thing.getUID().equals(thingUID)) {
215 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Device not present in response");
220 * Builds a {@link ThingUID} from a device model. The UID is build from the
221 * {@link AVMFritzBindingConstants#BINDING_ID} and value of
222 * {@link AVMFritzBaseModel#getProductName()} in which all characters NOT matching
223 * the regex [^a-zA-Z0-9_] are replaced by "_".
225 * @param device Discovered device model
226 * @return ThingUID without illegal characters.
229 public @Nullable ThingUID getThingUID(AVMFritzBaseModel device) {
230 ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, getThingTypeId(device).concat("_Solo"));
231 String ipAddress = getConfigAs(AVMFritzBoxConfiguration.class).ipAddress;
233 if (PL546E_STANDALONE_THING_TYPE.equals(thingTypeUID)) {
234 String thingName = "fritz.powerline".equals(ipAddress) ? ipAddress
235 : ipAddress.replaceAll(INVALID_PATTERN, "_");
236 return new ThingUID(thingTypeUID, thingName);
238 return super.getThingUID(device);
243 public void handleCommand(ChannelUID channelUID, Command command) {
244 String channelId = channelUID.getIdWithoutGroup();
245 logger.debug("Handle command '{}' for channel {}", command, channelId);
246 if (command == RefreshType.REFRESH) {
247 handleRefreshCommand();
250 FritzAhaWebInterface fritzBox = getWebInterface();
251 if (fritzBox == null) {
252 logger.debug("Cannot handle command '{}' because connection is missing", command);
255 String ain = getIdentifier();
257 logger.debug("Cannot handle command '{}' because AIN is missing", command);
263 case CHANNEL_DEVICE_LOCKED:
266 case CHANNEL_VOLTAGE:
267 logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command);
269 case CHANNEL_APPLY_TEMPLATE:
270 if (command instanceof StringType) {
271 fritzBox.applyTemplate(command.toString());
275 fritzBox.setSwitch(ain, OnOffType.ON.equals(command));
276 if (command instanceof OnOffType) {
278 state.getSwitch().setState(OnOffType.ON.equals(command) ? SwitchModel.ON : SwitchModel.OFF);
283 super.handleCommand(channelUID, command);
293 public @Nullable String getIdentifier() {