MQTT Button works

This commit is contained in:
Jan Zípek 2024-04-01 17:57:41 +02:00
parent 5e9e0cccc3
commit 6579a3ba52
Signed by: kamen
GPG Key ID: A17882625B33AC31
9 changed files with 171 additions and 5 deletions

21
client/src/api/mqtt.ts Normal file
View File

@ -0,0 +1,21 @@
import { request } from './request'
export const publishMqttMessage = (body: {
server: string
clientId?: string
username?: string
password?: string
qos?: number
retain?: boolean
message: string
topic: string
}) =>
request(
`/api/mqtt/publish`,
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
},
'void'
)

View File

@ -84,6 +84,10 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
&.hidden-title {
border-bottom: none;
}
.drag-handle { .drag-handle {
flex: 1; flex: 1;
cursor: move; cursor: move;
@ -133,6 +137,21 @@
justify-content: center; justify-content: center;
height: 100%; height: 100%;
} }
.mqtt-button {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
&.apply-stretch {
align-items: stretch;
> button {
flex: 1;
}
}
}
} }
} }

View File

@ -4,6 +4,7 @@ import { useConfirmModal } from '@/contexts/ConfirmModalsContext'
import { import {
DashboardDialData, DashboardDialData,
DashboardGraphData, DashboardGraphData,
DashboardMQTTButtonData,
} from '@/utils/dashboard/parseDashboard' } from '@/utils/dashboard/parseDashboard'
import { useForm } from '@/utils/hooks/useForm' import { useForm } from '@/utils/hooks/useForm'
import { useState } from 'preact/hooks' import { useState } from 'preact/hooks'
@ -11,6 +12,7 @@ import { useQuery } from 'react-query'
import { BoxDefinition } from '../../types' import { BoxDefinition } from '../../types'
import { DialSettings } from './components/DialSettings' import { DialSettings } from './components/DialSettings'
import { GraphSettings } from './components/GraphSettings' import { GraphSettings } from './components/GraphSettings'
import { MQTTButtonSettings } from './components/MQTTButtonSettings'
type Props = { type Props = {
value: BoxDefinition value: BoxDefinition
@ -63,6 +65,7 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
<select {...register('type')} required> <select {...register('type')} required>
<option value="graph">Graph</option> <option value="graph">Graph</option>
<option value="dial">Dial</option> <option value="dial">Dial</option>
<option value="mqttButton">MQTT Button</option>
</select> </select>
</div> </div>
@ -81,6 +84,13 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
sensors={sensors.data ?? []} sensors={sensors.data ?? []}
/> />
)} )}
{type === 'mqttButton' && (
<MQTTButtonSettings
value={data as DashboardMQTTButtonData}
onChange={setData}
/>
)}
</> </>
} }

View File

@ -0,0 +1,67 @@
import { FormCheckboxField } from '@/components/FormCheckboxField'
import { FormField } from '@/components/FormField'
import { DashboardMQTTButtonData } from '@/utils/dashboard/parseDashboard'
import { useForm } from '@/utils/hooks/useForm'
import { omit } from '@/utils/omit'
type Props = {
value?: DashboardMQTTButtonData
onChange: (data: DashboardMQTTButtonData) => void
}
export const MQTTButtonSettings = ({ value, onChange }: Props) => {
const { register } = useForm({
defaultValue: () => ({
server: '',
topic: '',
message: '',
retain: false,
...(value && omit(value, ['type'])),
}),
onChange: (v) => onChange({ ...v, type: 'mqttButton' }),
})
return (
<>
<FormField
name="server"
label="MQTT Broker Address"
hint="Example: tcp://10.10.1.1:1883"
>
<input required {...register('server')} />
</FormField>
<FormField name="clientId" label="Client ID">
<input {...register('clientId')} />
</FormField>
<FormField name="username" label="Username">
<input {...register('username')} />
</FormField>
<FormField name="password" label="Password">
<input type="password" {...register('password')} />
</FormField>
<FormField name="qoc" label="QoS">
<select {...register('qos', { type: 'integer' })}>
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
</select>
</FormField>
<FormCheckboxField name="retain" label="Retain">
<input type="checkbox" {...register('retain')} />
</FormCheckboxField>
<FormField name="topic" label="Topic">
<input required {...register('topic')} />
</FormField>
<FormField name="message" label="Payload">
<textarea required {...register('message')} rows={4} />
</FormField>
<FormCheckboxField
name="stretchButton"
label="Stretch button to full size"
hint="Makes button cover the whole area of the box"
>
<input type="checkbox" {...register('stretchButton')} />
</FormCheckboxField>
</>
)
}

View File

@ -0,0 +1,29 @@
import { DashboardMQTTButtonData } from '@/utils/dashboard/parseDashboard'
import { EditableBox, EditableBoxProps } from './EditableBox'
import { useMutation } from 'react-query'
import { publishMqttMessage } from '@/api/mqtt'
import { cn } from '@/utils/cn'
type Props = EditableBoxProps & {
data: DashboardMQTTButtonData
}
export const BoxMQTTButtonContent = ({ data, ...boxProps }: Props) => {
const pushMutation = useMutation(publishMqttMessage)
const onClick = () => {
pushMutation.mutate({
...data,
})
}
return (
<EditableBox {...boxProps} onRefresh={() => null} hiddenTitle>
<div className={cn('mqtt-button', data.stretchButton && 'apply-stretch')}>
<button onClick={onClick} disabled={pushMutation.isLoading}>
{boxProps.box.title ?? 'BUTTON'}
</button>
</div>
</EditableBox>
)
}

View File

@ -9,6 +9,7 @@ import { useWindowEvent } from '@/utils/hooks/useWindowEvent'
import { ComponentChild } from 'preact' import { ComponentChild } from 'preact'
import { useState } from 'preact/hooks' import { useState } from 'preact/hooks'
import { BoxSettings } from '../../BoxSettings/BoxSettings' import { BoxSettings } from '../../BoxSettings/BoxSettings'
import { cn } from '@/utils/cn'
export type EditableBoxProps = { export type EditableBoxProps = {
box: BoxDefinition box: BoxDefinition
@ -19,11 +20,13 @@ export type EditableBoxProps = {
} }
type EditableBoxPropsWithExtra = EditableBoxProps & { type EditableBoxPropsWithExtra = EditableBoxProps & {
hiddenTitle?: boolean
children: ComponentChild children: ComponentChild
onRefresh: () => void onRefresh: () => void
} }
export const EditableBox = ({ export const EditableBox = ({
hiddenTitle,
box, box,
children, children,
onPosition, onPosition,
@ -135,9 +138,9 @@ export const EditableBox = ({
} }
> >
<div className="box" style={{ height: '100%' }}> <div className="box" style={{ height: '100%' }}>
<div className="header"> <div className={cn('header', hiddenTitle && 'hidden-title')}>
<div className="drag-handle" onMouseDown={handleMouseDown}> <div className="drag-handle" onMouseDown={handleMouseDown}>
<div className="name">{box.title || ''}</div> {!hiddenTitle && <div className="name">{box.title || ''}</div>}
</div> </div>
<div className="actions"> <div className="actions">
<div className="action" onClick={onRefresh}> <div className="action" onClick={onRefresh}>
@ -148,6 +151,7 @@ export const EditableBox = ({
</div> </div>
</div> </div>
</div> </div>
<div className="body">{children}</div> <div className="body">{children}</div>
<div <div

View File

@ -1,5 +1,6 @@
import { BoxDialContent } from './BoxDialContent' import { BoxDialContent } from './BoxDialContent'
import { BoxGraphContent } from './BoxGraphContent' import { BoxGraphContent } from './BoxGraphContent'
import { BoxMQTTButtonContent } from './BoxMQTTButtonContent'
import { EditableBox, EditableBoxProps } from './EditableBox' import { EditableBox, EditableBoxProps } from './EditableBox'
export const GeneralBox = (props: EditableBoxProps) => { export const GeneralBox = (props: EditableBoxProps) => {
@ -8,10 +9,12 @@ export const GeneralBox = (props: EditableBoxProps) => {
return <BoxDialContent {...props} data={props.box.data} /> return <BoxDialContent {...props} data={props.box.data} />
case 'graph': case 'graph':
return <BoxGraphContent {...props} data={props.box.data} /> return <BoxGraphContent {...props} data={props.box.data} />
case 'mqttButton':
return <BoxMQTTButtonContent {...props} data={props.box.data} />
default: default:
return ( return (
<EditableBox {...props} onRefresh={() => null}> <EditableBox {...props} onRefresh={() => null}>
<></> <button onClick={() => props.onEdit(props.box)}>Pick a type</button>
</EditableBox> </EditableBox>
) )
} }

View File

@ -12,7 +12,7 @@ export type DashboardContentBox = {
w: number w: number
h: number h: number
title?: string title?: string
data?: DashboardGraphData | DashboardDialData data?: DashboardGraphData | DashboardDialData | DashboardMQTTButtonData
} }
export type DashboardGraphData = { export type DashboardGraphData = {
@ -44,6 +44,19 @@ export type DashboardDialData = {
fontSize?: number fontSize?: number
} }
export type DashboardMQTTButtonData = {
type: 'mqttButton'
server: string
clientId?: string
username?: string
password?: string
qos?: number
retain?: boolean
message: string
topic: string
stretchButton?: boolean
}
export const parseDashboard = (input: string) => { export const parseDashboard = (input: string) => {
const data = JSON.parse(input) as DashboardContent const data = JSON.parse(input) as DashboardContent
const migrations = getDashboardMigrations(data.version) const migrations = getDashboardMigrations(data.version)

View File

@ -9,7 +9,7 @@ import (
type putMQTTPublishBody struct { type putMQTTPublishBody struct {
Server string `json:"server" binding:"required"` Server string `json:"server" binding:"required"`
ClientId string `json:"clientId" binding:"required"` ClientId string `json:"clientId"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Retain *bool `json:"retain"` Retain *bool `json:"retain"`