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
11 changes: 10 additions & 1 deletion front/src/components/boxs/device-in-room/DeviceRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import SetpointDeviceFeature from './device-features/SetpointDeviceFeature';
import AirConditioningModeDeviceFeature from './device-features/AirConditioningModeDeviceFeature';
import PilotWireModeDeviceFeature from './device-features/PilotWireModeDeviceFeature';
import LMHVolumeDeviceFeature from './device-features/LMHVolumeDeviceFeature';
import MultiLevelWithInputDeviceFeature from './device-features/MultiLevelWithInputDeviceFeature';
import SirenModeDeviceFeature from './device-features/SirenModeDeviceFeature';
import SirenLevelDeviceFeature from './device-features/SirenLevelDeviceFeature';
import PushDeviceFeature from './device-features/PushDeviceFeature';

const ROW_TYPE_BY_FEATURE_TYPE = {
Expand All @@ -34,8 +37,14 @@ const ROW_TYPE_BY_FEATURE_TYPE = {
[DEVICE_FEATURE_TYPES.HEATER.PILOT_WIRE_MODE]: PilotWireModeDeviceFeature,
[DEVICE_FEATURE_TYPES.LOCK.BINARY]: BinaryDeviceFeature,
[DEVICE_FEATURE_TYPES.SIREN.LMH_VOLUME]: LMHVolumeDeviceFeature,
[DEVICE_FEATURE_TYPES.SIREN.MODE]: SirenModeDeviceFeature,
[DEVICE_FEATURE_TYPES.SIREN.LEVEL]: SirenLevelDeviceFeature,
[DEVICE_FEATURE_TYPES.SIREN.STROBE_LEVEL]: SirenLevelDeviceFeature,
[DEVICE_FEATURE_TYPES.SIREN.STROBE]: BinaryDeviceFeature,
[DEVICE_FEATURE_TYPES.SIREN.STROBE_DUTY_CYCLE]: NumberDeviceFeature,
[DEVICE_FEATURE_TYPES.SIREN.MELODY]: NumberDeviceFeature,
[DEVICE_FEATURE_TYPES.DURATION.DECIMAL]: MultiLevelDeviceFeature,
[DEVICE_FEATURE_TYPES.SIREN.VOLUME]: MultiLevelDeviceFeature,
[DEVICE_FEATURE_TYPES.DURATION.DECIMAL]: MultiLevelWithInputDeviceFeature,
[DEVICE_FEATURE_TYPES.BUTTON.PUSH]: PushDeviceFeature,
[DEVICE_FEATURE_TYPES.SWITCH.TARGET_CURRENT]: SetpointDeviceFeature,
[DEVICE_FEATURE_TYPES.ELECTRICAL_VEHICLE_CHARGE.CHARGE_ON]: BinaryDeviceFeature,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const SUPPORTED_FEATURE_TYPES = [
DEVICE_FEATURE_TYPES.LOCK.STATE,
DEVICE_FEATURE_TYPES.HEATER.PILOT_WIRE_MODE,
DEVICE_FEATURE_TYPES.SIREN.LMH_VOLUME,
DEVICE_FEATURE_TYPES.SIREN.VOLUME,
DEVICE_FEATURE_TYPES.SIREN.MELODY,
DEVICE_FEATURE_TYPES.DURATION.DECIMAL,
DEVICE_FEATURE_TYPES.BUTTON.PUSH,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Text } from 'preact-i18n';
import { useState, useEffect } from 'preact/hooks';
import get from 'get-value';
import cx from 'classnames';

import { DeviceFeatureCategoriesIcon } from '../../../../utils/consts';

import style from './style.css';

const MultiLevelWithInputDeviceFeature = ({ children, ...props }) => {
const { deviceFeature } = props;
const [localValue, setLocalValue] = useState(deviceFeature.last_value);

useEffect(() => {
setLocalValue(deviceFeature.last_value);
}, [deviceFeature.last_value]);

const clamp = value => {
let v = Number(value);
if (Number.isNaN(v)) {
return deviceFeature.min;
}
if (deviceFeature.min !== undefined && v < deviceFeature.min) v = deviceFeature.min;
if (deviceFeature.max !== undefined && v > deviceFeature.max) v = deviceFeature.max;
return v;
};

const handleSlider = e => {
const v = e.target.value;
setLocalValue(v);
props.updateValueWithDebounce(deviceFeature, v);
};
Comment on lines +28 to +32

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 | 🟡 Minor | ⚡ Quick win

handleSlider sends a string; commitInput sends a number — type inconsistency.

e.target.value on a range input is always a string, but commitInput uses clamp() which returns a Number. The backend/debounce handler likely expects a number, creating a silent type mismatch between the two code paths.

🛠️ Proposed fix
  const handleSlider = e => {
-   const v = e.target.value;
+   const v = Number(e.target.value);
    setLocalValue(v);
    props.updateValueWithDebounce(deviceFeature, v);
  };
📝 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
const handleSlider = e => {
const v = e.target.value;
setLocalValue(v);
props.updateValueWithDebounce(deviceFeature, v);
};
const handleSlider = e => {
const v = Number(e.target.value);
setLocalValue(v);
props.updateValueWithDebounce(deviceFeature, v);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front/src/components/boxs/device-in-room/device-features/MultiLevelWithInputDeviceFeature.jsx`
around lines 28 - 32, handleSlider currently reads e.target.value (a string) and
passes it to setLocalValue and props.updateValueWithDebounce, causing a type
mismatch with commitInput which uses Number from clamp(); fix by coercing the
slider value to a Number (e.g., Number(...) or parseFloat(...)) inside
handleSlider before calling setLocalValue and props.updateValueWithDebounce so
both code paths (handleSlider and commitInput) always work with numeric values;
reference functions: handleSlider, commitInput, setLocalValue,
props.updateValueWithDebounce, and clamp.


const handleInput = e => {
setLocalValue(e.target.value);
};

const commitInput = () => {
const v = clamp(localValue);
setLocalValue(v);
props.updateValueWithDebounce(deviceFeature, v);
};

const handleKeyDown = e => {
if (e.key === 'Enter') {
e.target.blur();
}
};

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

<td class={cx('text-right py-0', style.fullWidthCell)}>
<div class={style.stackedControl}>
<div class={style.numericRow}>
<input
type="number"
value={localValue}
onInput={handleInput}
onBlur={commitInput}
onKeyDown={handleKeyDown}
class={cx('form-control form-control-sm text-center px-1', style.numericInput)}
step={1}
min={deviceFeature.min}
max={deviceFeature.max}
/>
{deviceFeature.unit && (
<span class="ml-1">
<Text id={`deviceFeatureUnitShort.${deviceFeature.unit}`} />
</span>
)}
</div>
<input
type="range"
value={localValue}
onInput={handleSlider}
class={cx('custom-range', style.rangeInput)}
step="1"
min={deviceFeature.min}
max={deviceFeature.max}
/>
</div>
</td>
</tr>
);
};

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

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

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

function updateValue(e) {
props.updateValueWithDebounce(deviceFeature, e.currentTarget.value);
}

return (
<tr>
<td>
<i class={`fe fe-${get(DeviceFeatureCategoriesIcon, `${category}.${type}`, { default: 'sliders' })}`} />
</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={SIREN_LMH_VOLUME.LOW}>
<Text id={`deviceFeatureAction.category.${category}.${type}.low`} />
</option>
<option value={SIREN_LMH_VOLUME.MEDIUM}>
<Text id={`deviceFeatureAction.category.${category}.${type}.medium`} />
</option>
<option value={SIREN_LMH_VOLUME.HIGH}>
<Text id={`deviceFeatureAction.category.${category}.${type}.high`} />
</option>
<option value={SIREN_LMH_VOLUME.VERY_HIGH}>
<Text id={`deviceFeatureAction.category.${category}.${type}.very_high`} />
</option>
</select>
</div>
</div>
</td>
</tr>
);
};

export default SirenLevelDeviceFeature;
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 { SIREN_MODE } from '../../../../../../server/utils/constants';

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

function updateValue(e) {
props.updateValueWithDebounce(deviceFeature, e.currentTarget.value);
}

return (
<tr>
<td>
<i class={`fe fe-${get(DeviceFeatureCategoriesIcon, `${category}.${type}`, { default: 'sliders' })}`} />
</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={SIREN_MODE.STOP}>
<Text id={`deviceFeatureAction.category.${category}.${type}.stop`} />
</option>
<option value={SIREN_MODE.BURGLAR}>
<Text id={`deviceFeatureAction.category.${category}.${type}.burglar`} />
</option>
<option value={SIREN_MODE.FIRE}>
<Text id={`deviceFeatureAction.category.${category}.${type}.fire`} />
</option>
<option value={SIREN_MODE.EMERGENCY}>
<Text id={`deviceFeatureAction.category.${category}.${type}.emergency`} />
</option>
<option value={SIREN_MODE.POLICE_PANIC}>
<Text id={`deviceFeatureAction.category.${category}.${type}.police_panic`} />
</option>
<option value={SIREN_MODE.FIRE_PANIC}>
<Text id={`deviceFeatureAction.category.${category}.${type}.fire_panic`} />
</option>
<option value={SIREN_MODE.EMERGENCY_PANIC}>
<Text id={`deviceFeatureAction.category.${category}.${type}.emergency_panic`} />
</option>
</select>
</div>
</div>
</td>
</tr>
);
};

export default SirenModeDeviceFeature;
29 changes: 29 additions & 0 deletions front/src/components/boxs/device-in-room/device-features/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,32 @@ input[type='range'][class~='light-temperature']::-ms-fill-lower {
.rangeInput {
flex: 1;
}

.numericInput {
width: 5rem;
}

.fullWidthCell {
width: 100%;
}

.sliderRow {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
}

.stackedControl {
display: flex;
flex-direction: column;
width: 100%;
align-items: stretch;
}

.numericRow {
display: flex;
align-items: right;
justify-content: right;
margin-bottom: -0.5rem;
}
Comment on lines +82 to +87

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 | 🟡 Minor | ⚡ Quick win

align-items: right is not a valid CSS property value — browsers will silently ignore it.

align-items accepts values such as flex-start, flex-end, center, baseline, stretch, etc. right is not in the spec for this property (it is partially supported for justify-content in some browsers but not align-items). If vertical centering is intended, use align-items: center.

🛠️ Proposed fix
 .numericRow {
   display: flex;
-  align-items: right;
+  align-items: center;
   justify-content: flex-end;
   margin-bottom: -0.5rem;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front/src/components/boxs/device-in-room/device-features/style.css` around
lines 82 - 87, The .numericRow CSS uses invalid values: replace align-items:
right with a valid vertical alignment (e.g., align-items: center) and replace
justify-content: right with the proper horizontal value (justify-content:
flex-end) so the flex container behaves correctly; update the .numericRow rule
accordingly.

66 changes: 66 additions & 0 deletions front/src/components/device/SelectSirenMode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Component } from 'preact';
import { Text } from 'preact-i18n';
import Select from 'react-select';
import get from 'get-value';

import { SIREN_MODE } from '../../../../server/utils/constants';
import withIntlAsProp from '../../utils/withIntlAsProp';

class SelectSirenMode extends Component {
handleValueChange = ({ value }) => {
this.props.updateValue(value);
};

getOptions = () => {
const deviceFeatureOptions = Object.keys(SIREN_MODE).map(key => {
const value = SIREN_MODE[key];
return {
label: get(this.props.intl.dictionary, `deviceFeatureAction.category.siren.mode.${key.toLowerCase()}`, {
default: key.toLowerCase()
}),
value
};
});

this.setState({ deviceFeatureOptions });
};

getSelectedOption = () => {
const value = this.props.value;

if (value !== undefined && value !== null && value !== '') {
const numValue = Number(value);
const entry = Object.entries(SIREN_MODE).find(([, v]) => v === numValue);
const key = entry ? entry[0].toLowerCase() : String(value);
return {
label: get(this.props.intl.dictionary, `deviceFeatureAction.category.siren.mode.${key}`, {
default: key
}),
value: numValue
};
}
return undefined;
};

componentDidMount() {
this.getOptions();
}

render(props, { deviceFeatureOptions }) {
const selectedOption = this.getSelectedOption();
return (
<Select
class="select-device-feature"
defaultValue={''}
value={selectedOption}
onChange={this.handleValueChange}
options={deviceFeatureOptions}
placeholder={<Text id="global.selectPlaceholder" />}
className="react-select-container"
classNamePrefix="react-select"
/>
);
}
}

export default withIntlAsProp(SelectSirenMode);
33 changes: 32 additions & 1 deletion front/src/config/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -3310,6 +3310,27 @@
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch"
},
"mode": {
"stop": "Stopp",
"burglar": "Einbruch",
"fire": "Feuer",
"emergency": "Notfall",
"police_panic": "Polizei-Panik",
"fire_panic": "Feuer-Panik",
"emergency_panic": "Notfall-Panik"
},
"level": {
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch",
"very_high": "Sehr hoch"
},
"strobe_level": {
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch",
"very_high": "Sehr hoch"
}
}
}
Expand Down Expand Up @@ -3517,6 +3538,10 @@
}
},
"deviceFeatureCategory": {
"ac-connected": {
"shortCategoryName": "Netzbetrieb",
"binary": "Netzstrom verbunden (ja/nein)"
},
"light": {
"shortCategoryName": "Licht",
"binary": "Licht: ein/aus",
Expand Down Expand Up @@ -3622,7 +3647,13 @@
"shortCategoryName": "Sirene",
"binary": "Sirene",
"lmh_volume": "Lautstärke der Sirene",
"melody": "Melodie"
"volume": "Lautstärke der Sirene",
"melody": "Melodie",
"mode": "Warnmodus",
"level": "Lautstärke",
"strobe": "Blitzlicht",
"strobe_level": "Blitzintensität",
"strobe_duty_cycle": "Blitzzyklus"
},
"cube": {
"shortCategoryName": "Würfel",
Expand Down
Loading
Loading