Skip to content

Commit

Permalink
Implement Vacuum discovery for Homeassistant MQTT
Browse files Browse the repository at this point in the history
Closes openhab#8988

Signed-off-by: Stefan Triller <github@stefantriller.de>
  • Loading branch information
t2000 committed Nov 7, 2021
1 parent 56323f3 commit a141ac8
Show file tree
Hide file tree
Showing 3 changed files with 277 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public static class Builder {
private @Nullable String commandTopic;
private boolean retain;
private boolean trigger;
private boolean isAdvanced;
private @Nullable Integer qos;
private @Nullable Predicate<Command> commandFilter;

Expand All @@ -141,6 +142,7 @@ public Builder(AbstractComponent<?> component, String channelID, Value valueStat
this.channelID = channelID;
this.valueState = valueState;
this.label = label;
this.isAdvanced = false;
this.channelStateUpdateListener = channelStateUpdateListener;
}

Expand Down Expand Up @@ -194,6 +196,11 @@ public Builder trigger(boolean trigger) {
return this;
}

public Builder isAdvanced(boolean advanced) {
this.isAdvanced = advanced;
return this;
}

public Builder commandFilter(@Nullable Predicate<Command> commandFilter) {
this.commandFilter = commandFilter;
return this;
Expand Down Expand Up @@ -221,12 +228,13 @@ public ComponentChannel build(boolean addToComponent) {
String localStateTopic = stateTopic;
if (localStateTopic == null || localStateTopic.isBlank() || this.trigger) {
type = ChannelTypeBuilder.trigger(channelTypeUID, label)
.withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HA_CHANNEL)).build();
.withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HA_CHANNEL))
.isAdvanced(isAdvanced).build();
} else {
StateDescriptionFragment description = valueState.createStateDescription(commandTopic == null).build();
type = ChannelTypeBuilder.state(channelTypeUID, label, channelState.getItemType())
.withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HA_CHANNEL))
.withStateDescriptionFragment(description).build();
.withStateDescriptionFragment(description).isAdvanced(isAdvanced).build();
}

Configuration configuration = new Configuration();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID,
return new Sensor(componentConfiguration);
case "switch":
return new Switch(componentConfiguration);
case "vacuum":
return new Vacuum(componentConfiguration);
default:
throw new UnsupportedComponentException("Component '" + haID + "' is unsupported!");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.DateTimeValue;
import org.openhab.binding.mqtt.generic.values.NumberValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;

/**
* A MQTT vacuum, following the https://www.home-assistant.io/components/vacuum.mqtt/ specification.
*
* @author Stefan Triller - Initial contribution
*/
@NonNullByDefault
public class Vacuum extends AbstractComponent<Vacuum.ChannelConfiguration> {
public static final String vacuumStateChannelID = "state";
public static final String vacuumCommandChannelID = "command";
public static final String vacuumBatteryChannelID = "batteryLevel";
public static final String vacuumFanSpeedChannelID = "fanSpeed";

// sensor stats
public static final String vacuumMainBrushChannelID = "mainBrushUsage";
public static final String vacuumSideBrushChannelID = "sideBrushUsage";
public static final String vacuumFilterChannelID = "filter";
public static final String vacuumSensorChannelID = "sensor";
public static final String vacuumCurrentCleanTimeChannelID = "currentCleanTime";
public static final String vacuumCurrentCleanAreaChannelID = "currentCleanArea";
public static final String vacuumCleanTimeChannelID = "cleanTime";
public static final String vacuumCleanAreaChannelID = "cleanArea";
public static final String vacuumCleanCountChannelID = "cleanCount";

public static final String vacuumLastRunStartChannelID = "lastRunStart";
public static final String vacuumLastRunEndChannelID = "lastRunEnd";
public static final String vacuumLastRunDurationChannelID = "lastRunDuration";
public static final String vacuumLastRunAreaChannelID = "lastRunArea";
public static final String vacuumLastRunErrorCodeChannelID = "lastRunErrorCode";
public static final String vacuumLastRunErrorDescriptionChannelID = "lastRunErrorDescription";
public static final String vacuumLastRunFinishedFlagChannelID = "lastRunFinishedFlag";

public static final String vacuumBinInTimeChannelID = "binInTime";
public static final String vacuumLastBinOutChannelID = "lastBinOutTime";
public static final String vacuumlastBinFullChannelID = "lastBinFullTime";

public static final String vacuumCustomCommandChannelID = "customCommand";

/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Vacuum");
}

protected @Nullable String command_topic;
protected String state_topic = "";
protected @Nullable String send_command_topic; // for custom_command

// [start, pause, stop, return_home, battery, status, locate, clean_spot, fan_speed, send_command]
protected String[] supported_features = new String[] {};
protected @Nullable String set_fan_speed_topic;
protected String[] fan_speed_list = new String[] {};

protected @Nullable String json_attributes_topic;
protected @Nullable String json_attributes_template;
}

public Vacuum(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);

List<String> features = Arrays.asList(channelConfiguration.supported_features);

// features = [start, pause, stop, return_home, status, locate, clean_spot, fan_speed, send_command]
ArrayList<String> possibleCommands = new ArrayList<String>();
if (features.contains("start")) {
possibleCommands.add("start");
}

if (features.contains("stop")) {
possibleCommands.add("stop");
}

if (features.contains("pause")) {
possibleCommands.add("pause");
}

if (features.contains("return_home")) {
possibleCommands.add("return_to_base");
}

if (features.contains("locate")) {
possibleCommands.add("locate");
}

TextValue value = new TextValue(possibleCommands.toArray(new String[0]));
buildChannel(vacuumCommandChannelID, value, "Command", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.command_topic)
.commandTopic(channelConfiguration.command_topic, false, 1).build();

List<String> vacuumStates = List.of("docked", "cleaning", "returning", "paused", "idle", "error");
TextValue valueState = new TextValue(vacuumStates.toArray(new String[0]));
buildChannel(vacuumStateChannelID, valueState, "State", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.state_topic, "{{value_json.state}}").build();

if (features.contains("battery")) {
// build battery level channel (0-100)
NumberValue batValue = new NumberValue(BigDecimal.ZERO, new BigDecimal(100), new BigDecimal(1), "%");
buildChannel(vacuumBatteryChannelID, batValue, "Battery Level", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.state_topic, "{{value_json.battery_level}}").build();
}

if (features.contains("fan_speed")) {
// build fan speed channel with values from channelConfiguration.fan_speed_list
TextValue fanValue = new TextValue(channelConfiguration.fan_speed_list);
buildChannel(vacuumFanSpeedChannelID, fanValue, "Fan speed", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.state_topic, "{{value_json.fan_speed}}")
.commandTopic(channelConfiguration.set_fan_speed_topic, false, 1).build();
}

// {"mainBrush":"220.6","sideBrush":"120.6","filter":"70.6","sensor":"0.0","currentCleanTime":"0.0","currentCleanArea":"0.0","cleanTime":"79.3","cleanArea":"4439.9","cleanCount":183,"last_run_stats":{"startTime":1613503117000,"endTime":1613503136000,"duration":0,"area":"0.0","errorCode":0,"errorDescription":"No
// error","finishedFlag":false},"bin_in_time":1000,"last_bin_out":-1,"last_bin_full":-1,"last_loaded_map":null,"state":"docked","valetudo_state":{"id":8,"name":"Charging"}}
if (features.contains("status")) {
NumberValue currentCleanTimeValue = new NumberValue(null, null, null, null);
buildChannel(vacuumCurrentCleanTimeChannelID, currentCleanTimeValue, "Current Cleaning Time",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.currentCleanTime}}")
.build();

NumberValue currentCleanAreaValue = new NumberValue(null, null, null, null);
buildChannel(vacuumCurrentCleanAreaChannelID, currentCleanAreaValue, "Current Cleaning Area",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.currentCleanArea}}")
.build();

NumberValue cleanTimeValue = new NumberValue(null, null, null, null);
buildChannel(vacuumCleanTimeChannelID, cleanTimeValue, "Cleaning Time",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.cleanTime}}").build();

NumberValue cleanAreaValue = new NumberValue(null, null, null, null);
buildChannel(vacuumCleanAreaChannelID, cleanAreaValue, "Cleaned Area",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.cleanArea}}").build();

NumberValue cleaCountValue = new NumberValue(null, null, null, null);
buildChannel(vacuumCleanCountChannelID, cleaCountValue, "Cleaning Counter",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.cleanCount}}")
.build();

DateTimeValue lastStartTime = new DateTimeValue();
buildChannel(vacuumLastRunStartChannelID, lastStartTime, "Last run start time",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic,
"{{value_json.last_run_stats.startTime}}")
.build();

DateTimeValue lastEndTime = new DateTimeValue();
buildChannel(vacuumLastRunEndChannelID, lastEndTime, "Last run end time",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic,
"{{value_json.last_run_stats.endTime}}")
.build();

NumberValue lastRunDurationValue = new NumberValue(null, null, null, null);
buildChannel(vacuumLastRunDurationChannelID, lastRunDurationValue, "Last run duration",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic,
"{{value_json.last_run_stats.duration}}")
.build();

NumberValue lastRunAreaValue = new NumberValue(null, null, null, null);
buildChannel(vacuumLastRunAreaChannelID, lastRunAreaValue, "Last run area",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic,
"{{value_json.last_run_stats.area}}")
.build();

NumberValue lastRunErrorCodeValue = new NumberValue(null, null, null, null);
buildChannel(vacuumLastRunErrorCodeChannelID, lastRunErrorCodeValue, "Last run error code",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic,
"{{value_json.last_run_stats.errorCode}}")
.build();

TextValue lastRunErrorDescriptionValue = new TextValue();
buildChannel(vacuumLastRunErrorDescriptionChannelID, lastRunErrorDescriptionValue,
"Last run error description", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic,
"{{value_json.last_run_stats.errorDescription}}")
.build();

// true/false doesnt map to ON/OFF => use TextValue instead of OnOffValue
TextValue lastRunFinishedFlagValue = new TextValue();
buildChannel(vacuumLastRunFinishedFlagChannelID, lastRunFinishedFlagValue, "Last run finished flag",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic,
"{{value_json.last_run_stats.finishedFlag}}")
.build();

// only for valetudo re => advanced channels
DateTimeValue binInValue = new DateTimeValue();
buildChannel(vacuumBinInTimeChannelID, binInValue, "Bin In Time",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.bin_in_time}}")
.isAdvanced(true).build();

DateTimeValue lastBinOutValue = new DateTimeValue();
buildChannel(vacuumLastBinOutChannelID, lastBinOutValue, "Last Bin Out Time",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.last_bin_out}}")
.isAdvanced(true).build();

DateTimeValue lastBinFullValue = new DateTimeValue();
buildChannel(vacuumlastBinFullChannelID, lastBinFullValue, "Last Bin Full Time",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.last_bin_full}}")
.isAdvanced(true).build();
}

NumberValue mainBrush = new NumberValue(null, null, null, null);
buildChannel(vacuumMainBrushChannelID, mainBrush, "Main brush usage",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.mainBrush}}").build();

NumberValue sideBrush = new NumberValue(null, null, null, null);
buildChannel(vacuumSideBrushChannelID, sideBrush, "Side brush usage",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.sideBrush}}").build();

NumberValue filterValue = new NumberValue(null, null, null, null);
buildChannel(vacuumFilterChannelID, filterValue, "Filter time", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.filter}}").build();

NumberValue sensorValue = new NumberValue(null, null, null, null);
buildChannel(vacuumSensorChannelID, sensorValue, "Sensor", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.json_attributes_topic, "{{value_json.sensor}}").build();

// if we have a custom command channel for zone cleanup, etc => create text channel
if (channelConfiguration.send_command_topic != null) {
TextValue customCommandValue = new TextValue();
buildChannel(vacuumCustomCommandChannelID, customCommandValue, "Custom Command",
componentConfiguration.getUpdateListener())
.commandTopic(channelConfiguration.send_command_topic, false, 1)
.stateTopic(channelConfiguration.send_command_topic).build();
}
}
}

0 comments on commit a141ac8

Please sign in to comment.