-
-
Notifications
You must be signed in to change notification settings - Fork 315
Zigbee2MQTT: add WOOX R7051 smart siren #2477
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
base: master
Are you sure you want to change the base?
Changes from all commits
e9ce980
a5be84a
a5d8044
4919757
983cf53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| }; | ||
|
|
||
| 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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🛠️ Proposed fix .numericRow {
display: flex;
- align-items: right;
+ align-items: center;
justify-content: flex-end;
margin-bottom: -0.5rem;
}🤖 Prompt for AI Agents |
||
| 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleSlidersends a string;commitInputsends a number — type inconsistency.e.target.valueon a range input is always a string, butcommitInputusesclamp()which returns aNumber. 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
🤖 Prompt for AI Agents