2 * Copyright (c) 2010-2024 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.mielecloud.internal.webservice.api;
15 import java.util.List;
16 import java.util.Objects;
17 import java.util.Optional;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
22 import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
23 import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
24 import org.openhab.binding.mielecloud.internal.webservice.api.json.DryingStep;
25 import org.openhab.binding.mielecloud.internal.webservice.api.json.EcoFeedback;
26 import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
27 import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
28 import org.openhab.binding.mielecloud.internal.webservice.api.json.PlateStep;
29 import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramId;
30 import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramPhase;
31 import org.openhab.binding.mielecloud.internal.webservice.api.json.RemoteEnable;
32 import org.openhab.binding.mielecloud.internal.webservice.api.json.SpinningSpeed;
33 import org.openhab.binding.mielecloud.internal.webservice.api.json.State;
34 import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
35 import org.openhab.binding.mielecloud.internal.webservice.api.json.Status;
36 import org.openhab.binding.mielecloud.internal.webservice.api.json.Temperature;
37 import org.openhab.binding.mielecloud.internal.webservice.api.json.Type;
38 import org.openhab.binding.mielecloud.internal.webservice.api.json.VentilationStep;
41 * This immutable class provides methods to extract the device state information in a comfortable way.
43 * @author Roland Edelhoff - Initial contribution
44 * @author Björn Lange - Introduced null handling
45 * @author Benjamin Bolte - Add pre-heat finished, plate step, door state, door alarm, info state channel and map signal
47 * @author Björn Lange - Add elapsed time channel, dish warmer and robotic vacuum cleaner things, eco feedback
50 public class DeviceState {
52 private final String deviceIdentifier;
54 private final Optional<Device> device;
56 public DeviceState(String deviceIdentifier, @Nullable Device device) {
57 this.deviceIdentifier = deviceIdentifier;
58 this.device = Optional.ofNullable(device);
62 * Gets the unique identifier for this device.
64 * @return The unique identifier for this device.
66 public String getDeviceIdentifier() {
67 return deviceIdentifier;
71 * Gets the main operation status of the device.
73 * @return The main operation status of the device.
75 public Optional<String> getStatus() {
76 return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueLocalized);
80 * Gets the raw main operation status of the device.
82 * @return The raw main operation status of the device.
84 public Optional<Integer> getStatusRaw() {
85 return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueRaw);
89 * Gets the raw operation status of the device parsed to a {@link StateType}.
91 * @return The raw operation status of the device parsed to a {@link StateType}.
93 public Optional<StateType> getStateType() {
94 return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueRaw)
95 .flatMap(StateType::fromCode);
99 * Gets the currently selected program type of the device.
101 * @return The currently selected program type of the device.
103 public Optional<String> getSelectedProgram() {
104 if (deviceIsInOffState()) {
105 return Optional.empty();
107 return device.flatMap(Device::getState).flatMap(State::getProgramId).flatMap(ProgramId::getValueLocalized);
111 * Gets the selected program ID.
113 * @return The selected program ID.
115 public Optional<Long> getSelectedProgramId() {
116 if (deviceIsInOffState()) {
117 return Optional.empty();
119 return device.flatMap(Device::getState).flatMap(State::getProgramId).flatMap(ProgramId::getValueRaw);
123 * Gets the currently active phase of the active program.
125 * @return The currently active phase of the active program.
127 public Optional<String> getProgramPhase() {
128 if (deviceIsInOffState()) {
129 return Optional.empty();
131 return device.flatMap(Device::getState).flatMap(State::getProgramPhase)
132 .flatMap(ProgramPhase::getValueLocalized);
136 * Gets the currently active raw phase of the active program.
138 * @return The currently active raw phase of the active program.
140 public Optional<Integer> getProgramPhaseRaw() {
141 if (deviceIsInOffState()) {
142 return Optional.empty();
144 return device.flatMap(Device::getState).flatMap(State::getProgramPhase).flatMap(ProgramPhase::getValueRaw);
148 * Gets the currently selected drying step.
150 * @return The currently selected drying step.
152 public Optional<String> getDryingTarget() {
153 if (deviceIsInOffState()) {
154 return Optional.empty();
156 return device.flatMap(Device::getState).flatMap(State::getDryingStep).flatMap(DryingStep::getValueLocalized);
160 * Gets the currently selected raw drying step.
162 * @return The currently selected raw drying step.
164 public Optional<Integer> getDryingTargetRaw() {
165 if (deviceIsInOffState()) {
166 return Optional.empty();
168 return device.flatMap(Device::getState).flatMap(State::getDryingStep).flatMap(DryingStep::getValueRaw);
172 * Calculates if pre-heating the oven has finished.
174 * @return Whether pre-heating the oven has finished.
176 public Optional<Boolean> hasPreHeatFinished() {
177 if (deviceIsInOffState()) {
178 return Optional.empty();
181 Optional<Integer> targetTemperature = getTargetTemperature(0);
182 Optional<Integer> currentTemperature = getTemperature(0);
184 if (targetTemperature.isEmpty() || currentTemperature.isEmpty()) {
185 return Optional.empty();
188 return Optional.of(isInState(StateType.RUNNING) && currentTemperature.get() >= targetTemperature.get());
192 * Gets the target temperature with the given index.
194 * @return The target temperature with the given index.
196 public Optional<Integer> getTargetTemperature(int index) {
197 if (deviceIsInOffState()) {
198 return Optional.empty();
200 return device.flatMap(Device::getState).map(State::getTargetTemperature).flatMap(l -> getOrNull(l, index))
201 .flatMap(Temperature::getValueLocalized);
205 * Gets the current temperature of the device for the given index.
207 * @param index The index of the device zone for which the temperature shall be obtained.
208 * @return The target temperature if available.
210 public Optional<Integer> getTemperature(int index) {
211 if (deviceIsInOffState()) {
212 return Optional.empty();
215 return device.flatMap(Device::getState).map(State::getTemperature).flatMap(l -> getOrNull(l, index))
216 .flatMap(Temperature::getValueLocalized);
220 * Gets the remaining time of the active program.
222 * @return The remaining time in seconds.
224 public Optional<Integer> getRemainingTime() {
225 if (deviceIsInOffState()) {
226 return Optional.empty();
228 return device.flatMap(Device::getState).flatMap(State::getRemainingTime).flatMap(this::toSeconds);
232 * Gets the elapsed time of the active program.
234 * @return The elapsed time in seconds.
236 public Optional<Integer> getElapsedTime() {
237 if (deviceIsInOffState()) {
238 return Optional.empty();
240 return device.flatMap(Device::getState).flatMap(State::getElapsedTime).flatMap(this::toSeconds);
244 * Gets the relative start time of the active program.
246 * @return The delayed start time in seconds.
248 public Optional<Integer> getStartTime() {
249 if (deviceIsInOffState()) {
250 return Optional.empty();
252 return device.flatMap(Device::getState).flatMap(State::getStartTime).flatMap(this::toSeconds);
256 * Gets the "fullRemoteControl" state information of the device. If this flag is true ALL remote control actions
257 * of the device can be triggered.
259 * @return Whether the device can be remote controlled.
261 public Optional<Boolean> isRemoteControlEnabled() {
262 return device.flatMap(Device::getState).flatMap(State::getRemoteEnable)
263 .flatMap(RemoteEnable::getFullRemoteControl);
267 * Calculates the program process.
269 * @return The progress of the active program in percent.
271 public Optional<Integer> getProgress() {
272 if (deviceIsInOffState()) {
273 return Optional.empty();
276 Optional<Double> elapsedTime = device.flatMap(Device::getState).flatMap(State::getElapsedTime)
277 .flatMap(this::toSeconds).map(Integer::doubleValue);
278 Optional<Double> remainingTime = device.flatMap(Device::getState).flatMap(State::getRemainingTime)
279 .flatMap(this::toSeconds).map(Integer::doubleValue);
281 if (elapsedTime.isPresent() && remainingTime.isPresent()
282 && (elapsedTime.get() != 0 || remainingTime.get() != 0)) {
283 return Optional.of((int) ((elapsedTime.get() / (elapsedTime.get() + remainingTime.get())) * 100.0));
285 return Optional.empty();
289 private Optional<Integer> toSeconds(List<Integer> time) {
290 if (time.size() != 2) {
291 return Optional.empty();
293 return Optional.of((time.get(0) * 60 + time.get(1)) * 60);
297 * Gets the spinning speed.
299 * @return The spinning speed.
301 public Optional<String> getSpinningSpeed() {
302 if (deviceIsInOffState()) {
303 return Optional.empty();
305 return device.flatMap(Device::getState).flatMap(State::getSpinningSpeed).flatMap(SpinningSpeed::getValueRaw)
306 .map(String::valueOf);
310 * Gets the raw spinning speed.
312 * @return The raw spinning speed.
314 public Optional<Integer> getSpinningSpeedRaw() {
315 if (deviceIsInOffState()) {
316 return Optional.empty();
318 return device.flatMap(Device::getState).flatMap(State::getSpinningSpeed).flatMap(SpinningSpeed::getValueRaw);
322 * Gets the ventilation step.
324 * @return The ventilation step.
326 public Optional<String> getVentilationStep() {
327 if (deviceIsInOffState()) {
328 return Optional.empty();
330 return device.flatMap(Device::getState).flatMap(State::getVentilationStep)
331 .flatMap(VentilationStep::getValueLocalized).map(Object::toString);
335 * Gets the raw ventilation step.
337 * @return The raw ventilation step.
339 public Optional<Integer> getVentilationStepRaw() {
340 if (deviceIsInOffState()) {
341 return Optional.empty();
343 return device.flatMap(Device::getState).flatMap(State::getVentilationStep)
344 .flatMap(VentilationStep::getValueRaw);
348 * Gets the plate power step of the device for the given index.
350 * @param index The index of the device plate for which the power step shall be obtained.
351 * @return The plate power step if available.
353 public Optional<String> getPlateStep(int index) {
354 if (deviceIsInOffState()) {
355 return Optional.empty();
357 return device.flatMap(Device::getState).map(State::getPlateStep).flatMap(l -> getOrNull(l, index))
358 .flatMap(PlateStep::getValueLocalized);
362 * Gets the raw plate power step of the device for the given index.
364 * @param index The index of the device plate for which the power step shall be obtained.
365 * @return The raw plate power step if available.
367 public Optional<Integer> getPlateStepRaw(int index) {
368 if (deviceIsInOffState()) {
369 return Optional.empty();
371 return device.flatMap(Device::getState).map(State::getPlateStep).flatMap(l -> getOrNull(l, index))
372 .flatMap(PlateStep::getValueRaw);
376 * Gets the number of available plate steps.
378 * @return The number of available plate steps.
380 public Optional<Integer> getPlateStepCount() {
381 return device.flatMap(Device::getState).map(State::getPlateStep).map(List::size);
385 * Indicates if the device has an error that requires a user action.
387 * @return Whether the device has an error that requires a user action.
389 public boolean hasError() {
390 return isInState(StateType.FAILURE)
391 || device.flatMap(Device::getState).flatMap(State::getSignalFailure).orElse(false);
395 * Indicates if the device has a user information.
397 * @return Whether the device has a user information.
399 public boolean hasInfo() {
400 if (deviceIsInOffState()) {
403 return device.flatMap(Device::getState).flatMap(State::getSignalInfo).orElse(false);
407 * Gets the state of the light attached to the device.
409 * @return An {@link Optional} with value {@code true} if the light is turned on, {@code false} if the light is
410 * turned off or an empty {@link Optional} if light is not supported or no state is available.
412 public Optional<Boolean> getLightState() {
413 if (deviceIsInOffState()) {
414 return Optional.empty();
417 Optional<Light> light = device.flatMap(Device::getState).map(State::getLight);
418 if (light.isPresent()) {
419 if (light.get().equals(Light.ENABLE)) {
420 return Optional.of(true);
421 } else if (light.get().equals(Light.DISABLE)) {
422 return Optional.of(false);
426 return Optional.empty();
430 * Gets the state of the door attached to the device.
432 * @return Whether the device door is open.
434 public Optional<Boolean> getDoorState() {
435 if (deviceIsInOffState()) {
436 return Optional.empty();
439 return device.flatMap(Device::getState).flatMap(State::getSignalDoor);
443 * Gets the state of the device's door alarm.
445 * @return Whether the device door alarm was triggered.
447 public Optional<Boolean> getDoorAlarm() {
448 if (deviceIsInOffState()) {
449 return Optional.empty();
452 Optional<Boolean> doorState = getDoorState();
453 Optional<Boolean> failure = device.flatMap(Device::getState).flatMap(State::getSignalFailure);
455 if (doorState.isEmpty() || failure.isEmpty()) {
456 return Optional.empty();
459 return Optional.of(doorState.get() && failure.get());
463 * Gets the amount of water consumed since the currently running program started.
465 * @return The amount of water consumed since the currently running program started.
467 public Optional<Quantity> getCurrentWaterConsumption() {
468 if (deviceIsInOffState()) {
469 return Optional.empty();
472 return device.flatMap(Device::getState).flatMap(State::getEcoFeedback)
473 .flatMap(EcoFeedback::getCurrentWaterConsumption).flatMap(consumption -> consumption.getValue()
474 .map(value -> new Quantity(value, consumption.getUnit().orElse(null))));
478 * Gets the amount of energy consumed since the currently running program started.
480 * @return The amount of energy consumed since the currently running program started.
482 public Optional<Quantity> getCurrentEnergyConsumption() {
483 if (deviceIsInOffState()) {
484 return Optional.empty();
487 return device.flatMap(Device::getState).flatMap(State::getEcoFeedback)
488 .flatMap(EcoFeedback::getCurrentEnergyConsumption).flatMap(consumption -> consumption.getValue()
489 .map(value -> new Quantity(value, consumption.getUnit().orElse(null))));
493 * Gets the battery level.
495 * @return The battery level.
497 public Optional<Integer> getBatteryLevel() {
498 if (deviceIsInOffState()) {
499 return Optional.empty();
502 return device.flatMap(Device::getState).flatMap(State::getBatteryLevel);
506 * Gets the device type.
508 * @return The device type as human readable value.
510 public Optional<String> getType() {
511 return device.flatMap(Device::getIdent).flatMap(Ident::getType).flatMap(Type::getValueLocalized)
512 .filter(type -> !type.isEmpty());
516 * Gets the raw device type.
518 * @return The raw device type.
520 public DeviceType getRawType() {
521 return device.flatMap(Device::getIdent).flatMap(Ident::getType).map(Type::getValueRaw)
522 .orElse(DeviceType.UNKNOWN);
526 * Gets the user-defined name of the device.
528 * @return The user-defined name of the device.
530 public Optional<String> getDeviceName() {
531 return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceName).filter(name -> !name.isEmpty());
535 * Gets the fabrication (=serial) number of the device.
537 * @return The serial number of the device.
539 public Optional<String> getFabNumber() {
540 return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceIdentLabel)
541 .flatMap(DeviceIdentLabel::getFabNumber).filter(fabNumber -> !fabNumber.isEmpty());
545 * Gets the tech type of the device.
547 * @return The tech type of the device.
549 public Optional<String> getTechType() {
550 return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceIdentLabel)
551 .flatMap(DeviceIdentLabel::getTechType).filter(techType -> !techType.isEmpty());
554 private <T> Optional<T> getOrNull(List<T> list, int index) {
555 if (index < 0 || index >= list.size()) {
556 return Optional.empty();
559 return Optional.ofNullable(list.get(index));
562 private boolean deviceIsInOffState() {
563 return getStateType().map(StateType.OFF::equals).orElse(true);
566 public boolean isInState(StateType stateType) {
567 return getStateType().map(stateType::equals).orElse(false);
571 public int hashCode() {
572 return Objects.hash(device, deviceIdentifier);
576 public boolean equals(@Nullable Object obj) {
583 if (getClass() != obj.getClass()) {
586 DeviceState other = (DeviceState) obj;
587 return Objects.equals(device, other.device) && Objects.equals(deviceIdentifier, other.deviceIdentifier);