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.*;
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);
69 private @Nullable String identifier;
74 * @param bridge Bridge object representing a FRITZ!Powerline 546E
76 public Powerline546EHandler(Bridge bridge, HttpClient httpClient,
77 AVMFritzDynamicCommandDescriptionProvider commandDescriptionProvider) {
78 super(bridge, httpClient, commandDescriptionProvider);
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;
92 @SuppressWarnings({ "null", "unused" })
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);
100 devicelist.remove(device);
101 onDeviceUpdated(thing.getUID(), device);
103 onDeviceGone(thing.getUID());
105 super.onDeviceListAdded(devicelist);
109 public void onDeviceAdded(AVMFritzBaseModel device) {
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();
121 logger.debug("Update self '{}' with device model: {}", thingUID, device);
122 if (device.getPresent() == 1) {
123 updateStatus(ThingStatus.ONLINE);
125 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device not present");
128 updateProperties(device);
130 if (device.isPowermeter()) {
131 updatePowermeter(device.getPowermeter());
133 if (device.isSwitchableOutlet()) {
134 updateSwitchableOutlet(device.getSwitch());
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();
148 updateThingChannelState(CHANNEL_OUTLET, UnDefType.UNDEF);
150 updateThingChannelState(CHANNEL_OUTLET, SwitchModel.ON.equals(state) ? OnOffType.ON : OnOffType.OFF);
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));
164 * Updates thing properties.
166 * @param device the {@link AVMFritzBaseModel}
168 private void updateProperties(AVMFritzBaseModel device) {
169 Map<String, String> editProperties = editProperties();
170 editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
171 updateProperties(editProperties);
175 * Updates thing channels and creates dynamic channels if missing.
177 * @param channelId ID of the channel to be updated.
178 * @param state State to be set.
180 private void updateThingChannelState(String channelId, State state) {
181 Channel channel = thing.getChannel(channelId);
182 if (channel != null) {
183 updateState(channel.getUID(), state);
185 logger.debug("Channel '{}' in thing '{}' does not exist, recreating thing.", channelId, thing.getUID());
186 createChannel(channelId);
191 * Creates a {@link ChannelTypeUID} from the given channel id.
193 * @param channelId ID of the channel type UID to be created.
194 * @return the channel type UID
196 private ChannelTypeUID createChannelTypeUID(String channelId) {
197 final ChannelTypeUID channelTypeUID;
199 case CHANNEL_BATTERY:
200 channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_BATTERY_LEVEL.getUID();
202 case CHANNEL_VOLTAGE:
203 channelTypeUID = DefaultSystemChannelTypeProvider.SYSTEM_ELECTRIC_VOLTAGE.getUID();
206 channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
209 return channelTypeUID;
213 * Creates new channels for the thing.
215 * @param channelId ID of the channel to be created.
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());
228 public void onDeviceGone(ThingUID thingUID) {
229 if (thing.getUID().equals(thingUID)) {
230 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Device not present in response");
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 "_".
240 * @param device Discovered device model
241 * @return ThingUID without illegal characters.
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;
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);
253 return super.getThingUID(device);
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();
265 FritzAhaWebInterface fritzBox = getWebInterface();
266 if (fritzBox == null) {
267 logger.debug("Cannot handle command '{}' because connection is missing", command);
270 String ain = getIdentifier();
272 logger.debug("Cannot handle command '{}' because AIN is missing", command);
278 case CHANNEL_DEVICE_LOCKED:
281 case CHANNEL_VOLTAGE:
282 logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command);
284 case CHANNEL_APPLY_TEMPLATE:
285 if (command instanceof StringType) {
286 fritzBox.applyTemplate(command.toString());
290 if (command instanceof OnOffType) {
291 fritzBox.setSwitch(ain, OnOffType.ON.equals(command));
295 super.handleCommand(channelUID, command);
305 public @Nullable String getIdentifier() {