Skip to content

Commit

Permalink
Added switch for controlling of boolean states in non-expert mode: #2629
Browse files Browse the repository at this point in the history
  • Loading branch information
GermanBluefox committed Aug 23, 2024
1 parent 37e44c6 commit c986076
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 66 deletions.
25 changes: 23 additions & 2 deletions packages/admin/src-admin/src/components/ObjectBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ export interface TreeItemData {
title?: string;
/** if the item has "write" button (value=true, ack=false) */
button?: boolean;
/** If the item has read and write and is boolean */
switch?: boolean;
/** if the item has custom settings in `common.custom` */
hasCustoms?: boolean;
/** If this item is visible */
Expand Down Expand Up @@ -1622,6 +1624,8 @@ function buildTree(
typeof obj.common.role === 'string' &&
obj.common.role.startsWith('button') &&
obj.common?.write !== false,
switch: obj.type === 'state' && obj.common?.type === 'boolean' &&
obj.common?.write !== false && obj.common?.read !== false,
},
};

Expand Down Expand Up @@ -5343,8 +5347,20 @@ export class ObjectBrowserClass extends Component<ObjectBrowserProps, ObjectBrow
info.style = getValueStyle({ state, isExpertMode: this.state.filter.expertMode, isButton: item.data.button });

let val: React.JSX.Element[] = info.valTextRx as React.JSX.Element[];
if (!this.state.filter.expertMode && item.data.button) {
val = [<PressButtonIcon style={styles.cellValueButton} />];
if (!this.state.filter.expertMode) {
if (item.data.button) {
val = [<PressButtonIcon key="button" style={styles.cellValueButton} />];
} else if (item.data.switch) {
val = [<Switch
key="switch"
sx={{
'& .MuiSwitch-thumb': { color: info.style.color },
'& .MuiSwitch-track': !!this.states[id].val && this.state.selected.includes(id) ?
{ backgroundColor: this.props.themeType === 'dark' ? '#FFF !important' : '#111 !important' } : undefined,
}}
checked={!!this.states[id].val}
/>];
}
}

return <Tooltip
Expand Down Expand Up @@ -6442,6 +6458,11 @@ export class ObjectBrowserClass extends Component<ObjectBrowserProps, ObjectBrow
this.props.socket
.setState(id, true)
.catch(e => window.alert(`Cannot write state "${id}": ${e}`));
} else if (!this.state.filter.expertMode && item.data.switch) {
// in non-expert mode control switch directly
this.props.socket
.setState(id, !this.states[id].val)
.catch(e => window.alert(`Cannot write state "${id}": ${e}`));
} else {
this.edit = {
val: this.states[id] ? this.states[id].val : '',
Expand Down
8 changes: 3 additions & 5 deletions packages/jsonConfig/src/JsonConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,7 @@ class JsonConfig extends Router<JsonConfigProps, JsonConfigState> {
schema,
data: obj.native,
common: obj.common,
// @ts-expect-error really no string?
hash: MD5(JSON.stringify(schema)),
hash: MD5(JSON.stringify(schema)).toString(),
});
} else {
window.alert(`Instance system.adapter.${this.props.adapterName}.${this.props.instance} not found!`);
Expand Down Expand Up @@ -345,8 +344,7 @@ class JsonConfig extends Router<JsonConfigProps, JsonConfigState> {
} else if (this.fileSubscribed.includes(fileName)) {
try {
const schema = await this.getConfigFile(this.fileSubscribed[0]);
// @ts-expect-error really no string?
this.setState({ schema, hash: MD5(JSON.stringify(schema)) });
this.setState({ schema, hash: MD5(JSON.stringify(schema)).toString() });
} catch {
// ignore errors
}
Expand Down Expand Up @@ -596,7 +594,7 @@ class JsonConfig extends Router<JsonConfigProps, JsonConfigState> {

for (const attr of Object.keys(this.state.data)) {
const item = this.findAttr(attr);
if ((!item || !item.doNotSave) && !attr.startsWith('_')) {
if ((!item || !item.doNotSave || item.type === 'state') && !attr.startsWith('_')) {
ConfigGeneric.setValue(obj.native, attr, this.state.data[attr]);
} else {
ConfigGeneric.setValue(obj.native, attr, null);
Expand Down
6 changes: 4 additions & 2 deletions packages/jsonConfig/src/JsonConfigComponent/ConfigGeneric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export default class ConfigGeneric<Props extends ConfigGenericProps = ConfigGene
props.globalData,
)
: props.schema.default;
} else {
} else if (props.schema.type !== 'state') {
this.defaultValue = props.schema.defaultFunc
? this.execute(
props.schema.defaultFunc,
Expand All @@ -188,7 +188,9 @@ export default class ConfigGeneric<Props extends ConfigGenericProps = ConfigGene
}

componentDidMount() {
this.props.registerOnForceUpdate && this.props.registerOnForceUpdate(this.props.attr, this.onUpdate);
if (this.props.registerOnForceUpdate) {
this.props.registerOnForceUpdate(this.props.attr, this.onUpdate);
}
const LIKE_SELECT = ['select', 'autocomplete', 'autocompleteSendTo'];
// init default value
if (this.defaultValue !== undefined) {
Expand Down
115 changes: 78 additions & 37 deletions packages/jsonConfig/src/JsonConfigComponent/ConfigState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,38 @@ interface ConfigStateProps extends ConfigGenericProps {
interface ConfigStateState extends ConfigGenericState {
stateValue?: string | number | boolean | null;
controlType?: string;
obj?: ioBroker.Object | null;
}

class ConfigState extends ConfigGeneric<ConfigStateProps, ConfigStateState> {
obj: ioBroker.Object | null = null;

controlTimeout: ReturnType<typeof setTimeout> | null = null;

delayedUpdate: { timer: ReturnType<typeof setTimeout> | null, value: string | boolean | number | null } = { timer: null, value: null };

getObjectID() {
return `${this.props.schema.system ? 'system.adapter.' : ''}${this.props.adapterName}.${this.props.instance}.${this.props.schema.oid}`;
}

async componentDidMount() {
super.componentDidMount();
this.obj = await this.props.socket.getObject(this.getObjectID());
const controlType = this.props.schema.control || await this.detectType();
const obj: ioBroker.StateObject = await this.props.socket.getObject(this.getObjectID()) as ioBroker.StateObject;
const controlType = this.props.schema.control || await this.detectType(obj);

const state = await this.props.socket.getState(this.getObjectID());

this.setState({ stateValue: state ? state.val : null, controlType }, async () => {
this.setState({ stateValue: state ? state.val : null, controlType, obj }, async () => {
await this.props.socket.subscribeState(this.getObjectID(), this.onStateChanged);
});
}

componentWillUnmount() {
super.componentWillUnmount();
this.props.socket.unsubscribeState(this.getObjectID(), this.onStateChanged);
if (this.delayedUpdate.timer) {
clearTimeout(this.delayedUpdate.timer);
this.delayedUpdate.timer = null;
}

if (this.controlTimeout) {
clearTimeout(this.controlTimeout);
this.controlTimeout = null;
Expand All @@ -60,45 +66,69 @@ class ConfigState extends ConfigGeneric<ConfigStateProps, ConfigStateState> {
this.state.controlType === 'switch'
) {
val = !!val;
if (this.state.stateValue !== val) {
this.setState({ stateValue: val });
}
} else if (val !== null && (this.state.controlType === 'slider' || this.state.controlType === 'number')) {
val = parseFloat(val as unknown as string);
console.log(`${Date.now()} Received new value: ${val}`);
if (val !== this.state.stateValue) {
if (this.delayedUpdate.timer) {
clearTimeout(this.delayedUpdate.timer);
this.delayedUpdate.timer = null;
}
this.delayedUpdate.value = val;
this.delayedUpdate.timer = setTimeout(() => {
this.setState({ stateValue: this.delayedUpdate.value });
}, 500);
} else if (this.delayedUpdate.timer) {
clearTimeout(this.delayedUpdate.timer);
this.delayedUpdate.timer = null;
}
} else if (this.state.stateValue.toString() !== val.toString()) {
this.setState({ stateValue: val });
}

this.setState({ stateValue: val });
};

async detectType() {
async detectType(obj: ioBroker.StateObject) {
obj = obj || {} as ioBroker.StateObject;
obj.common = obj.common || {} as ioBroker.StateCommon;

// read object
if (this.obj.common.type === 'boolean') {
if (this.obj.common.read === false) {
if (obj.common.type === 'boolean') {
if (obj.common.read === false) {
return 'button';
}
if (this.obj.common.write) {
if (obj.common.write) {
return 'switch';
}

return 'text';
}

if (this.obj.common.type === 'number') {
if (this.obj.common.write) {
if (this.obj.common.max !== undefined) {
if (obj.common.type === 'number') {
if (obj.common.write) {
if (obj.common.max !== undefined) {
return 'slider';
}
return 'input';
}
return 'text';
}

if (this.obj.common.write) {
if (obj.common.write) {
return 'input';
}

return 'text';
}

renderItem(/* error, disabled, defaultValue */) {
let content: React.JSX.Element | null = null;
if (!this.state.obj) {
return null;
}

let content: React.JSX.Element;

if (this.state.controlType === 'button') {
let icon: React.JSX.Element | null = null;
Expand All @@ -113,6 +143,7 @@ class ConfigState extends ConfigGeneric<ConfigStateProps, ConfigStateState> {
</IconButton>;
} else {
content = <Button
variant={this.props.schema.variant || 'contained'}
startIcon={icon}
style={this.props.schema.falseTextStyle}
>
Expand Down Expand Up @@ -143,7 +174,7 @@ class ConfigState extends ConfigGeneric<ConfigStateProps, ConfigStateState> {
textTrue ||
iconTrue
) {
content = <div style={{ display: 'flex' }}>
content = <div style={{ display: 'flex', alignItems: 'center', fontSize: 14 }}>
<span style={this.props.schema.falseTextStyle}>
{textFalse}
{iconFalse}
Expand All @@ -158,8 +189,8 @@ class ConfigState extends ConfigGeneric<ConfigStateProps, ConfigStateState> {

const label = this.getText(this.props.schema.label, this.props.schema.noTranslation);
if (label) {
content = <div style={{ display: 'flex' }}>
{label}
content = <div style={{ display: 'flex', alignItems: 'center', fontSize: '1rem' }}>
<span style={{ marginRight: 8 }}>{label}</span>
{content}
</div>;
}
Expand All @@ -175,24 +206,25 @@ class ConfigState extends ConfigGeneric<ConfigStateProps, ConfigStateState> {
iconTrue = getIconByName(this.props.schema.trueImage, textTrue ? { marginRight: 8 } : undefined);
}

const min = this.props.schema.min === undefined ? this.obj.common.min || 0 : this.props.schema.min;
const max = this.props.schema.max === undefined ? (this.obj.common.max === undefined ? 100 : this.obj.common.max) : this.props.schema.max;
const step = this.props.schema.step === undefined ? this.obj.common.step || 1 : this.props.schema.step;
const min = this.props.schema.min === undefined ? this.state.obj.common.min || 0 : this.props.schema.min;
const max = this.props.schema.max === undefined ? (this.state.obj.common.max === undefined ? 100 : this.state.obj.common.max) : this.props.schema.max;
const step = this.props.schema.step === undefined ? this.state.obj.common.step || 1 : this.props.schema.step;

content = <Slider
style={{ width: '100%', flexGrow: 1 }}
min={min}
max={max}
step={step}
value={this.state.stateValue as number}
onChange={(e: Event, value: number) => {
onChange={(_e: Event, value: number) => {
this.setState({ stateValue: value }, async () => {
if (this.controlTimeout) {
clearTimeout(this.controlTimeout);
}
this.controlTimeout = setTimeout(async () => {
console.log(`${Date.now()} Send new value: ${this.state.stateValue}`);
this.controlTimeout = null;
await this.props.socket.setState(this.getObjectID(), value, false);
await this.props.socket.setState(this.getObjectID(), this.state.stateValue, false);
}, this.props.schema.controlDelay || 0);
});
}}
Expand All @@ -203,29 +235,37 @@ class ConfigState extends ConfigGeneric<ConfigStateProps, ConfigStateState> {
textTrue ||
iconTrue
) {
content = <div style={{ display: 'flex', width: '100%', flexGrow: 1 }}>
<span style={this.props.schema.falseTextStyle}>
content = <div
style={{
display: 'flex',
width: '100%',
flexGrow: 1,
alignItems: 'center',
}}
>
<span style={{ marginRight: 16, ...this.props.schema.falseTextStyle }}>
{textFalse}
{iconFalse}
</span>
{content}
<span style={this.props.schema.trueTextStyle}>
<span style={{ marginLeft: 16, ...this.props.schema.trueTextStyle }}>
{iconTrue}
{textTrue}
</span>
</div>;
}
const label = this.getText(this.props.schema.label, this.props.schema.noTranslation);
if (label) {
content = <div style={{ display: 'flex', width: '100%' }}>
{label}
content = <div style={{ display: 'flex', width: '100%', alignItems: 'center' }}>
<span style={{ whiteSpace: 'nowrap', marginRight: 8, fontSize: '1rem' }}>{label}</span>
{content}
</div>;
}
} else if (this.state.controlType === 'input') {
content = <TextField
style={{ width: '100%' }}
value={this.state.stateValue}
variant="standard"
onChange={e => {
this.setState({ stateValue: e.target.value }, async () => {
if (this.controlTimeout) {
Expand All @@ -240,12 +280,13 @@ class ConfigState extends ConfigGeneric<ConfigStateProps, ConfigStateState> {
label={this.getText(this.props.schema.label)}
helperText={this.renderHelp(this.props.schema.help, this.props.schema.helpLink, this.props.schema.noTranslation)}
/>;
} else if (this.obj.common.type === 'number') {
const min = this.props.schema.min === undefined ? this.obj.common.min || 0 : this.props.schema.min;
const max = this.props.schema.max === undefined ? (this.obj.common.max === undefined ? 100 : this.obj.common.max) : this.props.schema.max;
const step = this.props.schema.step === undefined ? this.obj.common.step || 1 : this.props.schema.step;
} else if (this.state.obj.common.type === 'number') {
const min = this.props.schema.min === undefined ? this.state.obj.common.min || 0 : this.props.schema.min;
const max = this.props.schema.max === undefined ? (this.state.obj.common.max === undefined ? 100 : this.state.obj.common.max) : this.props.schema.max;
const step = this.props.schema.step === undefined ? this.state.obj.common.step || 1 : this.props.schema.step;

content = <TextField
variant="standard"
style={{ width: '100%' }}
value={this.state.stateValue}
type="number"
Expand All @@ -265,7 +306,7 @@ class ConfigState extends ConfigGeneric<ConfigStateProps, ConfigStateState> {
label={this.getText(this.props.schema.label)}
helperText={this.renderHelp(this.props.schema.help, this.props.schema.helpLink, this.props.schema.noTranslation)}
/>;
} else if (this.obj.common.type === 'boolean') {
} else if (this.state.obj.common.type === 'boolean') {
let icon: React.JSX.Element | null = null;
let text: string;
let style: React.CSSProperties | undefined;
Expand All @@ -283,16 +324,16 @@ class ConfigState extends ConfigGeneric<ConfigStateProps, ConfigStateState> {
style = this.props.schema.trueTextStyle;
}
const label = this.getText(this.props.schema.label, this.props.schema.noTranslation);
content = <div style={style}>
content = <div style={{ fontSize: '1rem', ...style }}>
{label}
{label ? <span style={{ marginRight: 8 }}>:</span> : null}
{icon}
{text || (this.state.stateValue ? I18n.t('ra_true') : I18n.t('ra_false'))}
</div>;
} else {
const label = this.getText(this.props.schema.label, this.props.schema.noTranslation);
const unit = this.getText(this.props.schema.unit, this.props.schema.noTranslation) || this.obj.common.unit;
content = <div>
const unit = this.getText(this.props.schema.unit, this.props.schema.noTranslation) || this.state.obj.common.unit;
content = <div style={{ fontSize: '1rem' }}>
{label}
{label ? <span style={{ marginRight: 8 }}>:</span> : null}
{this.state.stateValue}
Expand Down
Loading

0 comments on commit c986076

Please sign in to comment.