Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mqtt.homeassistant] Implement Vacuum discovery for Homeassistant MQTT #11216

Merged
merged 3 commits into from
Nov 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,264 @@
/**
* 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 VACUUM_STATE_CHANNEL_ID = "state";
public static final String VACUUM_COMMAND_CHANNEL_ID = "command";
public static final String VACUUM_BATTERY_CHANNEL_ID = "batteryLevel";
public static final String VACUUM_FAN_SPEED_CHANNEL_ID = "fanSpeed";

// sensor stats
public static final String VACUUM_MAIN_BRUSH_CHANNEL_ID = "mainBrushUsage";
public static final String VACUUM_SIDE_BRUSH_CHANNEL_ID = "sideBrushUsage";
public static final String VACUUM_FILTER_CHANNEL_ID = "filter";
public static final String VACUUM_SENSOR_CHANNEL_ID = "sensor";
public static final String VACUUM_CURRENT_CLEAN_TIME_CHANNEL_ID = "currentCleanTime";
public static final String VACUUM_CURRENT_CLEAN_AREA_CHANNEL_ID = "currentCleanArea";
public static final String VACUUM_CLEAN_TIME_CHANNEL_ID = "cleanTime";
public static final String VACUUM_CLEAN_AREA_CHANNEL_ID = "cleanArea";
public static final String VACUUM_CLEAN_COUNT_CHANNEL_ID = "cleanCount";

public static final String VACUUM_LAST_RUN_START_CHANNEL_ID = "lastRunStart";
public static final String VACUUM_LAST_RUN_END_CHANNEL_ID = "lastRunEnd";
public static final String VACUUM_LAST_RUN_DURATION_CHANNEL_ID = "lastRunDuration";
public static final String VACUUM_LAST_RUN_AREA_CHANNEL_ID = "lastRunArea";
public static final String VACUUM_LAST_RUN_ERROR_CODE_CHANNEL_ID = "lastRunErrorCode";
public static final String VACUUM_LAST_RUN_ERROR_DESCRIPTION_CHANNEL_ID = "lastRunErrorDescription";
public static final String VACUUM_LAST_RUN_FINISHED_FLAG_CHANNEL_ID = "lastRunFinishedFlag";

public static final String VACUUM_BIN_IN_TIME_CHANNEL_ID = "binInTime";
public static final String VACUUM_LAST_BIN_OUT_TIME_CHANNEL_ID = "lastBinOutTime";
public static final String VACUUM_LAST_BIN_FULL_TIME_CHANNEL_ID = "lastBinFullTime";

public static final String VACUUM_CUSMTOM_COMMAND_CHANNEL_ID = "customCommand";

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

protected @Nullable String commandTopic;
protected String stateTopic = "";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work?
Don't we need a @SerializedName("state_topic") vor the deserialization to work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yes!

Thank you very much. I have overlooked this and just blindly integrated the review comments as I knew they made sense in a way that there are styles on how to name variables.

I have created a small PR to fix this: #11550

protected @Nullable String sendCommandTopic; // for custom_command

// [start, pause, stop, return_home, battery, status, locate, clean_spot, fan_speed, send_command]
protected String[] supportedFeatures = new String[] {};
protected @Nullable String setFanSpeedTopic;
protected String[] fanSpeedList = new String[] {};

protected @Nullable String jsonAttributesTopic;
protected @Nullable String jsonAttributesTemplate;
}

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

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

// 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(VACUUM_COMMAND_CHANNEL_ID, value, "Command", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.commandTopic).commandTopic(channelConfiguration.commandTopic, false, 1)
.build();

List<String> vacuumStates = List.of("docked", "cleaning", "returning", "paused", "idle", "error");
TextValue valueState = new TextValue(vacuumStates.toArray(new String[0]));
buildChannel(VACUUM_STATE_CHANNEL_ID, valueState, "State", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, "{{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(VACUUM_BATTERY_CHANNEL_ID, batValue, "Battery Level",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, "{{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.fanSpeedList);
buildChannel(VACUUM_FAN_SPEED_CHANNEL_ID, fanValue, "Fan speed", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, "{{value_json.fan_speed}}")
.commandTopic(channelConfiguration.setFanSpeedTopic, 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(VACUUM_CURRENT_CLEAN_TIME_CHANNEL_ID, currentCleanTimeValue, "Current Cleaning Time",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.jsonAttributesTopic, "{{value_json.currentCleanTime}}")
.build();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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