Skip to content

Commit c199394

Browse files
pedrolamasclaude
andauthored
feat: adds Moonraker sensor charts (#1885)
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1f87be8 commit c199394

17 files changed

Lines changed: 278 additions & 58 deletions

File tree

src/components/ui/AppInlineChart.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,15 @@ export default class AppInlineChart extends Vue {
6868
6969
return this.labels.map(label => {
7070
const value = get(item, label.value)
71+
const formattedValue = typeof value === 'number' && value !== 0
72+
? value.toFixed(2)
73+
: value
7174
7275
return {
7376
label,
7477
value: this.isEmpty(value)
7578
? '--'
76-
: `${value}${label.suffix ?? ''}`
79+
: `${formattedValue}${label.suffix ?? ''}`
7780
}
7881
})
7982
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<template>
2+
<app-inline-chart
3+
:data="chartData"
4+
:options="options"
5+
:labels="labels"
6+
/>
7+
</template>
8+
9+
<script lang="ts">
10+
import { Component, Prop, Vue } from 'vue-property-decorator'
11+
import type { EChartsOption, LineSeriesOption } from 'echarts'
12+
import type { ChartData } from '@/store/charts/types'
13+
import type { AppInlineChartLabel } from '@/components/ui/AppInlineChart.vue'
14+
15+
@Component({})
16+
export default class SensorChart extends Vue {
17+
@Prop({ type: String, required: true })
18+
readonly sensorId!: string
19+
20+
@Prop({ type: String, required: true })
21+
readonly field!: string
22+
23+
@Prop({ type: String, required: true })
24+
readonly label!: string
25+
26+
@Prop({ type: String })
27+
readonly units?: string
28+
29+
get chartData (): ChartData[] {
30+
return this.$typedState.charts[`sensor:${this.sensorId}`] ?? []
31+
}
32+
33+
get suffix (): string {
34+
return this.units
35+
? ` ${this.units}`
36+
: ''
37+
}
38+
39+
get labels (): AppInlineChartLabel[] {
40+
return [
41+
{
42+
text: this.label,
43+
value: this.field,
44+
suffix: this.suffix
45+
}
46+
]
47+
}
48+
49+
get options (): EChartsOption {
50+
const options: EChartsOption = {
51+
...this.$typedGetters['charts/getBaseChartOptions']({
52+
[this.field]: this.suffix
53+
}),
54+
series: this.series
55+
}
56+
57+
if (
58+
options.yAxis &&
59+
!Array.isArray(options.yAxis)
60+
) {
61+
options.yAxis.min = (value) => Math.min(0, value.min)
62+
options.yAxis.max = (value) => Math.max(1, value.max * 1.1)
63+
}
64+
65+
return options
66+
}
67+
68+
get series (): LineSeriesOption {
69+
return {
70+
...this.$typedGetters['charts/getBaseSeries'],
71+
name: this.label,
72+
encode: {
73+
x: 'date',
74+
y: this.field
75+
}
76+
}
77+
}
78+
}
79+
</script>
Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,118 @@
11
<template>
2-
<v-container>
3-
<v-row
2+
<v-expansion-panels
3+
v-model="expanded"
4+
accordion
5+
multiple
6+
>
7+
<v-expansion-panel
48
v-for="sensor in sensors"
59
:key="sensor.id"
610
>
7-
<v-col>
8-
{{ $filters.prettyCase(sensor.friendly_name) }}
9-
10-
<v-chip
11-
v-for="(value, key) in sensor.values"
12-
:key="`${sensor.id}-${key}`"
13-
small
14-
class="ml-2"
11+
<v-expansion-panel-header>
12+
<template #actions>
13+
<v-icon
14+
dense
15+
class="mr-1"
16+
>
17+
$expand
18+
</v-icon>
19+
</template>
20+
<div>
21+
{{ $filters.prettyCase(sensor.friendly_name) }}
22+
23+
<v-chip
24+
v-for="(value, key) in sensor.values"
25+
:key="`${sensor.id}-${key}`"
26+
small
27+
class="ml-2"
28+
>
29+
{{ $filters.prettyCase(key.toString()) }}: {{ getFormattedValue(sensor, key.toString(), value) }}
30+
</v-chip>
31+
</div>
32+
</v-expansion-panel-header>
33+
34+
<v-expansion-panel-content>
35+
<v-row v-if="getChartableFields(sensor).length">
36+
<sensor-chart
37+
v-for="field in getChartableFields(sensor)"
38+
:key="`${sensor.id}-chart-${field}`"
39+
:sensor-id="sensor.id"
40+
:field="field"
41+
:label="$filters.prettyCase(field)"
42+
:units="getUnits(sensor, field)"
43+
/>
44+
</v-row>
45+
46+
<div
47+
v-else
48+
class="text--secondary"
1549
>
16-
{{ $filters.prettyCase(key.toString()) }}: {{ getFormattedValue(sensor, key.toString(), value) }}
17-
</v-chip>
18-
</v-col>
19-
</v-row>
20-
</v-container>
50+
{{ $t('app.general.label.no_data') }}
51+
</div>
52+
</v-expansion-panel-content>
53+
</v-expansion-panel>
54+
</v-expansion-panels>
2155
</template>
2256

2357
<script lang="ts">
2458
import { Component, Vue } from 'vue-property-decorator'
59+
import SensorChart from './SensorChart.vue'
2560
26-
@Component({})
61+
@Component({
62+
components: {
63+
SensorChart
64+
}
65+
})
2766
export default class Sensors extends Vue {
2867
get sensors (): Moonraker.Sensor.Entry[] {
2968
return this.$typedGetters['sensors/getSensors']
3069
}
3170
71+
get expanded (): number[] {
72+
const sensors = this.sensors
73+
const expandedKeys = this.$typedState.sensors.expanded
74+
75+
return sensors
76+
.map((sensor, index) => expandedKeys.includes(sensor.id)
77+
? index
78+
: -1)
79+
.filter(i => i !== -1)
80+
}
81+
82+
set expanded (value: number[]) {
83+
const sensors = this.sensors
84+
const expandedKeys = value
85+
.map(index => sensors[index]?.id)
86+
.filter((id): id is string => id != null)
87+
88+
this.$typedDispatch('sensors/saveExpanded', expandedKeys)
89+
}
90+
91+
getChartableFields (sensor: Moonraker.Sensor.Entry): string[] {
92+
return Object.entries(sensor.values)
93+
.filter(([, value]) => typeof value === 'number')
94+
.map(([key]) => key)
95+
}
96+
97+
getUnits (sensor: Moonraker.Sensor.Entry, key: string): string | undefined {
98+
return sensor.parameter_info?.find(x => x.name === key)?.units
99+
}
100+
32101
getFormattedValue (sensor: Moonraker.Sensor.Entry, key: string, value: unknown) {
33102
if (value == null || value === '') {
34103
return '--'
35104
}
36105
37-
const parameterUnits = sensor.parameter_info?.find(x => x.name === key)?.units
38-
const units = parameterUnits
39-
? ` ${parameterUnits}`
106+
const units = this.getUnits(sensor, key)
107+
const suffix = units
108+
? ` ${units}`
40109
: ''
41110
42111
if (typeof value === 'number') {
43-
return `${Math.round(value * 100) / 100}${units}`
112+
return `${Math.round(value * 100) / 100}${suffix}`
44113
}
45114
46-
return `${value}${units}`
115+
return `${value}${suffix}`
47116
}
48117
}
49118
</script>

src/components/widgets/system/KlipperLoadChart.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ export default class KlipperLoadChart extends Vue {
2222
{
2323
text: this.$t('app.system_info.label.klipper_load').toString(),
2424
value: 'cputime_change',
25-
suffix: '%'
25+
suffix: ' %'
2626
}
2727
]
2828
}
2929
3030
get options (): EChartsOption {
3131
return {
3232
...this.$typedGetters['charts/getBaseChartOptions']({
33-
cputime_change: '%'
33+
cputime_change: ' %'
3434
}),
3535
series: this.series
3636
}

src/components/widgets/system/McuLoadChart.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,21 @@ export default class McuLoadChart extends Vue {
2626
{
2727
text: this.$t('app.system_info.label.mcu_load', { mcu: this.mcu.prettyName }).toString(),
2828
value: 'load',
29-
suffix: '%'
29+
suffix: ' %'
3030
},
3131
{
3232
text: this.$t('app.system_info.label.mcu_awake', { mcu: this.mcu.prettyName }).toString(),
3333
value: 'awake',
34-
suffix: '%'
34+
suffix: ' %'
3535
}
3636
]
3737
}
3838
3939
get options (): EChartsOption {
4040
const options: EChartsOption = {
4141
...this.$typedGetters['charts/getBaseChartOptions']({
42-
load: '%',
43-
awake: '%',
42+
load: ' %',
43+
awake: ' %',
4444
bw: 'b'
4545
}),
4646
series: this.series

src/components/widgets/system/MoonrakerLoadChart.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ export default class MoonrakerLoadChart extends Vue {
2222
{
2323
text: this.$t('app.system_info.label.moonraker_load').toString(),
2424
value: 'load',
25-
suffix: '%'
25+
suffix: ' %'
2626
}
2727
]
2828
}
2929
3030
get options (): EChartsOption {
3131
return {
3232
...this.$typedGetters['charts/getBaseChartOptions']({
33-
load: '%'
33+
load: ' %'
3434
}),
3535
series: this.series
3636
}

src/components/widgets/system/SystemMemoryChart.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ export default class SystemMemoryChart extends Vue {
2222
{
2323
text: this.$t('app.system_info.label.system_memory').toString(),
2424
value: 'memused',
25-
suffix: '%'
25+
suffix: ' %'
2626
}
2727
]
2828
}
2929
3030
get options (): EChartsOption {
3131
return {
3232
...this.$typedGetters['charts/getBaseChartOptions']({
33-
memused: '%'
33+
memused: ' %'
3434
}),
3535
series: this.series
3636
}

src/globals.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ export const Globals = Object.freeze({
249249
ROOTS: {
250250
uiSettings: { name: 'uiSettings', dispatch: 'config/initUiSettings' },
251251
macros: { name: 'macros', dispatch: 'macros/initMacros' },
252+
sensors: { name: 'sensors', dispatch: 'sensors/initSensors' },
252253
console: { name: 'console', dispatch: 'console/initConsole' },
253254
charts: { name: 'charts', dispatch: 'charts/initCharts' },
254255
cameras: { name: 'cameras', dispatch: 'webcams/initLegacyCameras', migrate_only: true },

src/locales/en.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ app:
413413
moonraker: Moonraker
414414
name: Name
415415
new_password: New password
416+
no_data: No data
416417
no_notifications: No notifications
417418
'on': 'On'
418419
'off': 'Off'

0 commit comments

Comments
 (0)