Skip to content
Open
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
8 changes: 7 additions & 1 deletion front/src/components/boxs/device-in-room/DeviceRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import AirConditioningModeDeviceFeature from './device-features/AirConditioningM
import PilotWireModeDeviceFeature from './device-features/PilotWireModeDeviceFeature';
import LMHVolumeDeviceFeature from './device-features/LMHVolumeDeviceFeature';
import PushDeviceFeature from './device-features/PushDeviceFeature';
import VacuumCleanerDockDeviceFeature from './device-features/VacuumCleanerDockDeviceFeature';
import VacuumCleanerModeDeviceFeature from './device-features/VacuumCleanerModeDeviceFeature';
import VacuumCleanerCleanModeDeviceFeature from './device-features/VacuumCleanerCleanModeDeviceFeature';

const ROW_TYPE_BY_FEATURE_TYPE = {
[DEVICE_FEATURE_TYPES.LIGHT.BINARY]: BinaryDeviceFeature,
Expand Down Expand Up @@ -44,7 +47,10 @@ const ROW_TYPE_BY_FEATURE_TYPE = {
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_CLIMATE.CLIMATE_ON]: BinaryDeviceFeature,
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_CLIMATE.TARGET_TEMPERATURE]: SetpointDeviceFeature,
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_COMMAND.ALARM]: BinaryDeviceFeature,
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_COMMAND.LOCK]: BinaryDeviceFeature
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_COMMAND.LOCK]: BinaryDeviceFeature,
[DEVICE_FEATURE_TYPES.VACUUM_CLEANER.DOCK]: VacuumCleanerDockDeviceFeature,
[DEVICE_FEATURE_TYPES.VACUUM_CLEANER.RUN_MODE]: VacuumCleanerModeDeviceFeature,
[DEVICE_FEATURE_TYPES.VACUUM_CLEANER.CLEAN_MODE]: VacuumCleanerCleanModeDeviceFeature
};

const DeviceRow = ({ children, ...props }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import get from 'get-value';
import { Text } from 'preact-i18n';

import { DeviceFeatureCategoriesIcon } from '../../../../utils/consts';
import { VACUUM_CLEANER_CLEAN_MODE } from '../../../../../../server/utils/constants';

const VacuumCleanerCleanModeDeviceFeature = ({ children, ...props }) => {
const { deviceFeature } = props;
const { category, type } = deviceFeature;

function updateValue(e) {
props.updateValueWithDebounce(deviceFeature, e.currentTarget.value);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<tr>
<td>
<i class={`fe fe-${get(DeviceFeatureCategoriesIcon, `${category}.${type}`, { default: 'settings' })}`} />
</td>
<td>{props.rowName}</td>

<td class="py-0">
<div class="justify-content-end">
<div class="form-group mb-0">
<select value={props.deviceFeature.last_value} onChange={updateValue} class="form-control form-control-sm">
<option value={VACUUM_CLEANER_CLEAN_MODE.AUTO}>
<Text id={`deviceFeatureAction.category.vacuum-cleaner.clean-mode.auto`} />
</option>
<option value={VACUUM_CLEANER_CLEAN_MODE.QUICK}>
<Text id={`deviceFeatureAction.category.vacuum-cleaner.clean-mode.quick`} />
</option>
<option value={VACUUM_CLEANER_CLEAN_MODE.QUIET}>
<Text id={`deviceFeatureAction.category.vacuum-cleaner.clean-mode.quiet`} />
</option>
<option value={VACUUM_CLEANER_CLEAN_MODE.LOW_NOISE}>
<Text id={`deviceFeatureAction.category.vacuum-cleaner.clean-mode.low-noise`} />
</option>
<option value={VACUUM_CLEANER_CLEAN_MODE.DEEP_CLEAN}>
<Text id={`deviceFeatureAction.category.vacuum-cleaner.clean-mode.deep-clean`} />
</option>
<option value={VACUUM_CLEANER_CLEAN_MODE.VACUUM}>
<Text id={`deviceFeatureAction.category.vacuum-cleaner.clean-mode.vacuum`} />
</option>
<option value={VACUUM_CLEANER_CLEAN_MODE.MOP}>
<Text id={`deviceFeatureAction.category.vacuum-cleaner.clean-mode.mop`} />
</option>
</select>
</div>
</div>
</td>
</tr>
);
};

export default VacuumCleanerCleanModeDeviceFeature;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Component } from 'preact';
import cx from 'classnames';
import { Text } from 'preact-i18n';
import style from './style.css';

class VacuumCleanerDockDeviceFeature extends Component {
constructor(props) {
super(props);
this.state = {
loading: false
};
}

dock = async () => {
await this.setState({ loading: true });
this.props.updateValue(this.props.deviceFeature, 1);
setTimeout(() => {
this.setState({ loading: false });
}, 350);
};
Comment on lines +14 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Preact class components, does setState return a Promise that can be awaited?

💡 Result:

No, in Preact class components, setState does not return a Promise that can be awaited. It returns void.

Citations:


Tie loading state to the actual command promise and prevent duplicate dock requests.

await this.setState(...) has no effect in Preact (setState returns void), and resetting loading after a fixed timeout (350ms) can re-enable clicks before updateValue completes or fails. The button also lacks the disabled attribute to prevent duplicate submissions during the request.

Proposed fix
 class VacuumCleanerDockDeviceFeature extends Component {
@@
   dock = async () => {
-    await this.setState({ loading: true });
-    this.props.updateValue(this.props.deviceFeature, 1);
-    setTimeout(() => {
-      this.setState({ loading: false });
-    }, 350);
+    if (this.state.loading) {
+      return;
+    }
+    this.setState({ loading: true });
+    try {
+      await this.props.updateValue(this.props.deviceFeature, 1);
+    } finally {
+      this.setState({ loading: false });
+    }
   };
@@
           <button
             onClick={this.dock}
             type="button"
+            disabled={loading}
             class={cx('btn', 'btn-outline-primary', 'btn-sm', style.btnLoading, {
               'btn-loading': loading
             })}
           >

Also applies to: 30-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front/src/components/boxs/device-in-room/device-features/VacuumCleanerDockDeviceFeature.jsx`
around lines 14 - 20, The dock method currently uses await this.setState(...)
(ineffective in Preact) and a fixed 350ms timeout to clear loading, which can
re-enable the UI before the command completes and allows duplicate requests;
change dock to set loading:true synchronously via this.setState({loading:true}),
call the promise-returning this.props.updateValue(this.props.deviceFeature, 1)
and await it, catch errors, then setState({loading:false}) in finally; also
update the button/render that triggers dock to include
disabled={this.state.loading} (or equivalent) so clicks are prevented while the
request is inflight; apply the same pattern to the other similar method(s)
referenced at lines 30-35.


render(props, { loading }) {
return (
<tr>
<td>
<i class="fe fe-home" />
</td>
<td>{props.rowName}</td>
<td class="text-right">
<button
onClick={this.dock}
type="button"
class={cx('btn', 'btn-outline-primary', 'btn-sm', style.btnLoading, {
'btn-loading': loading
})}
>
<i class="fe fe-home" /> <Text id="dashboard.boxes.devicesInRoom.vacuumDock" />
</button>
</td>
</tr>
);
}
}

export default VacuumCleanerDockDeviceFeature;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import get from 'get-value';
import { Text } from 'preact-i18n';

import { DeviceFeatureCategoriesIcon } from '../../../../utils/consts';
import { VACUUM_CLEANER_MODE } from '../../../../../../server/utils/constants';

const VacuumCleanerModeDeviceFeature = ({ children, ...props }) => {
const { deviceFeature } = props;
const { category, type } = deviceFeature;

function updateValue(e) {
props.updateValueWithDebounce(deviceFeature, e.currentTarget.value);
}
Comment on lines +11 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cast selected mode to number before updating value.

e.currentTarget.value is a string. Forwarding it as-is can break numeric mode mapping/commands downstream.

🔧 Proposed fix
 function updateValue(e) {
-  props.updateValueWithDebounce(deviceFeature, e.currentTarget.value);
+  props.updateValueWithDebounce(deviceFeature, Number(e.currentTarget.value));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function updateValue(e) {
props.updateValueWithDebounce(deviceFeature, e.currentTarget.value);
}
function updateValue(e) {
props.updateValueWithDebounce(deviceFeature, Number(e.currentTarget.value));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front/src/components/boxs/device-in-room/device-features/VacuumCleanerModeDeviceFeature.jsx`
around lines 11 - 13, The updateValue handler forwards a string from
e.currentTarget.value which should be a number; modify function updateValue to
convert e.currentTarget.value to a Number (e.g., using Number(...) or
parseInt/parseFloat as appropriate) before calling props.updateValueWithDebounce
with deviceFeature and the numeric value so downstream numeric mode
mapping/commands receive a number instead of a string.


return (
<tr>
<td>
<i class={`fe fe-${get(DeviceFeatureCategoriesIcon, `${category}.${type}`, { default: 'settings' })}`} />
</td>
<td>{props.rowName}</td>

<td class="py-0">
<div class="justify-content-end">
<div class="form-group mb-0">
<select value={props.deviceFeature.last_value} onChange={updateValue} class="form-control form-control-sm">
<option value={VACUUM_CLEANER_MODE.IDLE}>
<Text id={`deviceFeatureAction.category.vacuum-cleaner.mode.idle`} />
</option>
<option value={VACUUM_CLEANER_MODE.CLEANING}>
<Text id={`deviceFeatureAction.category.vacuum-cleaner.mode.cleaning`} />
</option>
<option value={VACUUM_CLEANER_MODE.MAPPING}>
<Text id={`deviceFeatureAction.category.vacuum-cleaner.mode.mapping`} />
</option>
</select>
</div>
</div>
</td>
</tr>
);
};

export default VacuumCleanerModeDeviceFeature;
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import NoRecentValueBadge from './NoRecentValueBadge';
import TemperatureSensorDeviceValue from './TemperatureSensorDeviceValue';
import LevelSensorDeviceValue from './LevelSensorDeviceValue';
import PressureSensorDeviceValue from './PressureSensorDeviceValue';
import VacuumCleanerStateDeviceValue from './VacuumCleanerStateDeviceValue';

const DISPLAY_BY_FEATURE_CATEGORY = {
[DEVICE_FEATURE_CATEGORIES.MOTION_SENSOR]: MotionSensorDeviceValue,
Expand Down Expand Up @@ -50,7 +51,8 @@ const DISPLAY_BY_FEATURE_TYPE = {
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_CLIMATE.INDOOR_TEMPERATURE]: TemperatureSensorDeviceValue,
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_BATTERY.BATTERY_RANGE_ESTIMATE]: DistanceSensorDeviceValue,
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_STATE.ODOMETER]: DistanceSensorDeviceValue,
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_STATE.TIRE_PRESSURE]: PressureSensorDeviceValue
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_STATE.TIRE_PRESSURE]: PressureSensorDeviceValue,
[DEVICE_FEATURE_TYPES.VACUUM_CLEANER.STATE]: VacuumCleanerStateDeviceValue
};

const DEVICE_FEATURES_WITHOUT_EXPIRATION = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Text } from 'preact-i18n';
import cx from 'classnames';

const VacuumCleanerStateDeviceValue = props => {
const { last_value: lastValue = null } = props.deviceFeature;
const valued = lastValue !== null;

return (
<span
class={cx('badge', {
'bg-primary': valued,
'bg-secondary': !valued
})}
>
{!valued && <Text id="dashboard.boxes.devicesInRoom.noValue" />}
{valued && (
<Text id={`deviceFeatureValue.category.vacuum-cleaner.state.${lastValue}`}>
<Text id={`deviceFeatureValue.category.vacuum-cleaner.state.unknown`} fields={{ value: lastValue }} />
</Text>
)}
</span>
);
};

export default VacuumCleanerStateDeviceValue;
38 changes: 37 additions & 1 deletion front/src/config/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@
"addButton": "+",
"substractButton": "-",
"motionDetected": "Bewegung erkannt",
"pushButton": "Schieben"
"pushButton": "Schieben",
"vacuumDock": "Zurück zur Ladestation"
},
"devices": {
"editDeviceFeaturesLabel": "Wähle die Geräte aus, die du anzeigen möchtest:",
Expand Down Expand Up @@ -3373,6 +3374,22 @@
"medium": "Mittel",
"high": "Hoch"
}
},
"vacuum-cleaner": {
"mode": {
"idle": "Leerlauf",
"cleaning": "Reinigen",
"mapping": "Kartieren"
},
"clean-mode": {
"auto": "Auto",
"quick": "Schnell",
"quiet": "Leise",
"low-noise": "Geräuscharm",
"deep-clean": "Tiefenreinigung",
"vacuum": "Saugen",
"mop": "Wischen"
}
}
}
},
Expand Down Expand Up @@ -3577,6 +3594,18 @@
"3": "Hoch",
"4": "Netz kritisch"
}
},
"vacuum-cleaner": {
"state": {
"unknown": "{{value}} (unbekannt)",
"0": "Gestoppt",
"1": "Läuft",
"2": "Pausiert",
"3": "Fehler",
"4": "Kehrt zur Ladestation zurück",
"5": "Lädt",
"6": "Angedockt"
}
}
}
},
Expand Down Expand Up @@ -4042,6 +4071,13 @@
"unknown": {
"shortCategoryName": "Unbekannt",
"unknown": "Unbekannt"
},
"vacuum-cleaner": {
"shortCategoryName": "Saugroboter",
"state": "Betriebszustand",
"run-mode": "Betriebsmodus",
"clean-mode": "Reinigungsmodus",
"dock": "Zurück zur Ladestation"
}
},
"errorPage": {
Expand Down
38 changes: 37 additions & 1 deletion front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@
"addButton": "+",
"substractButton": "-",
"motionDetected": "Motion detected",
"pushButton": "Push"
"pushButton": "Push",
"vacuumDock": "Return to Dock"
},
"devices": {
"editDeviceFeaturesLabel": "Select the devices you want to display:",
Expand Down Expand Up @@ -3373,6 +3374,22 @@
"medium": "Medium",
"high": "High"
}
},
"vacuum-cleaner": {
"mode": {
"idle": "Idle",
"cleaning": "Clean",
"mapping": "Map"
},
"clean-mode": {
"auto": "Auto",
"quick": "Quick",
"quiet": "Quiet",
"low-noise": "Low Noise",
"deep-clean": "Deep Clean",
"vacuum": "Vacuum",
"mop": "Mop"
}
}
}
},
Expand Down Expand Up @@ -3577,6 +3594,18 @@
"3": "High",
"4": "Critical"
}
},
"vacuum-cleaner": {
"state": {
"unknown": "{{value}} (unknown)",
"0": "Stopped",
"1": "Running",
"2": "Paused",
"3": "Error",
"4": "Returning to Dock",
"5": "Charging",
"6": "Docked"
}
}
}
},
Expand Down Expand Up @@ -4042,6 +4071,13 @@
"unknown": {
"shortCategoryName": "Unknown",
"unknown": "Unknown"
},
"vacuum-cleaner": {
"shortCategoryName": "Vacuum Cleaner",
"state": "Operational State",
"run-mode": "Run Mode",
"clean-mode": "Clean Mode",
"dock": "Return to Dock"
}
},
"errorPage": {
Expand Down
Loading
Loading