diff --git a/webapp/backend/pkg/database/scrutiny_repository_migrations.go b/webapp/backend/pkg/database/scrutiny_repository_migrations.go
index e1b948b..9a02574 100644
--- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go
+++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go
@@ -332,7 +332,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
SettingDataType: "string",
SettingValueString: "smooth",
},
-
{
SettingKeyName: "metrics.notify_level",
SettingKeyDescription: "Determines which device status will cause a notification (fail or warn)",
@@ -385,6 +384,21 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return tx.Create(&defaultSettings).Error
},
},
+ {
+ ID: "m20240722082740", // add powered_on_hours_unit setting.
+ Migrate: func(tx *gorm.DB) error {
+ //add powered_on_hours_unit setting default.
+ var defaultSettings = []m20220716214900.Setting{
+ {
+ SettingKeyName: "powered_on_hours_unit",
+ SettingKeyDescription: "Presentation format for device powered on time ('humanize' | 'device_hours')",
+ SettingDataType: "string",
+ SettingValueString: "humanize",
+ },
+ }
+ return tx.Create(&defaultSettings).Error
+ },
+ },
})
if err := m.Migrate(); err != nil {
@@ -421,8 +435,8 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
// helpers
-//When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy.
-//This function will ignore retention policy errors, and allow the migration to continue.
+// When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy.
+// This function will ignore retention policy errors, and allow the migration to continue.
func ignorePastRetentionPolicyError(err error) error {
var influxDbWriteError *http.Error
if errors.As(err, &influxDbWriteError) {
diff --git a/webapp/backend/pkg/models/settings.go b/webapp/backend/pkg/models/settings.go
index e564301..d40e3e9 100644
--- a/webapp/backend/pkg/models/settings.go
+++ b/webapp/backend/pkg/models/settings.go
@@ -8,13 +8,14 @@ package models
//}
type Settings struct {
- Theme string `json:"theme" mapstructure:"theme"`
- Layout string `json:"layout" mapstructure:"layout"`
- DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
- DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
- TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
- FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
- LineStroke string `json:"line_stroke" mapstructure:"line_stroke"`
+ Theme string `json:"theme" mapstructure:"theme"`
+ Layout string `json:"layout" mapstructure:"layout"`
+ DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
+ DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
+ TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
+ FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
+ LineStroke string `json:"line_stroke" mapstructure:"line_stroke"`
+ PoweredOnHoursUnit string `json:"powered_on_hours_unit" mapstructure:"powered_on_hours_unit"`
Metrics struct {
NotifyLevel int `json:"notify_level" mapstructure:"notify_level"`
diff --git a/webapp/frontend/src/app/core/config/app.config.ts b/webapp/frontend/src/app/core/config/app.config.ts
index 1b6dfe3..c5a1f6f 100644
--- a/webapp/frontend/src/app/core/config/app.config.ts
+++ b/webapp/frontend/src/app/core/config/app.config.ts
@@ -12,6 +12,8 @@ export type TemperatureUnit = 'celsius' | 'fahrenheit'
export type LineStroke = 'smooth' | 'straight' | 'stepline'
+export type DevicePoweredOnUnit = 'humanize' | 'device_hours'
+
export enum MetricsNotifyLevel {
Warn = 1,
@@ -47,6 +49,8 @@ export interface AppConfig {
file_size_si_units?: boolean;
+ powered_on_hours_unit?: DevicePoweredOnUnit;
+
line_stroke?: LineStroke;
// Settings from Scrutiny API
@@ -77,6 +81,7 @@ export const appConfig: AppConfig = {
temperature_unit: 'celsius',
file_size_si_units: false,
+ powered_on_hours_unit: 'humanize',
line_stroke: 'smooth',
diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html
index 43e8964..43f4e0b 100644
--- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html
+++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html
@@ -63,7 +63,7 @@
Powered On
-
{{ humanizeDuration(deviceSummary.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}
+
{{ deviceSummary.smart?.power_on_hours | deviceHours:config.powered_on_hours_unit:{ round: true, largest: 1, units: ['y', 'd', 'h'] } }}
--
diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts
index 57da104..c54bbc7 100644
--- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts
+++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts
@@ -4,7 +4,6 @@ import {takeUntil} from 'rxjs/operators';
import {AppConfig} from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {Subject} from 'rxjs';
-import humanizeDuration from 'humanize-duration'
import {MatDialog} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
@@ -34,8 +33,6 @@ export class DashboardDeviceComponent implements OnInit {
private _unsubscribeAll: Subject;
- readonly humanizeDuration = humanizeDuration;
-
deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold
ngOnInit(): void {
diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html
index 0eb9a03..970044c 100644
--- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html
+++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html
@@ -54,6 +54,14 @@
+
+ Powered On Format
+
+ Humanize
+ Device Hours
+
+
+
Line stroke
diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts
index 94f262f..ec65157 100644
--- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts
+++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts
@@ -7,7 +7,8 @@ import {
MetricsStatusThreshold,
TemperatureUnit,
LineStroke,
- Theme
+ Theme,
+ DevicePoweredOnUnit
} from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {Subject} from 'rxjs';
@@ -24,6 +25,7 @@ export class DashboardSettingsComponent implements OnInit {
dashboardSort: string;
temperatureUnit: string;
fileSizeSIUnits: boolean;
+ poweredOnHoursUnit: string;
lineStroke: string;
theme: string;
statusThreshold: number;
@@ -51,6 +53,7 @@ export class DashboardSettingsComponent implements OnInit {
this.dashboardSort = config.dashboard_sort;
this.temperatureUnit = config.temperature_unit;
this.fileSizeSIUnits = config.file_size_si_units;
+ this.poweredOnHoursUnit = config.powered_on_hours_unit;
this.lineStroke = config.line_stroke;
this.theme = config.theme;
@@ -68,6 +71,7 @@ export class DashboardSettingsComponent implements OnInit {
dashboard_sort: this.dashboardSort as DashboardSort,
temperature_unit: this.temperatureUnit as TemperatureUnit,
file_size_si_units: this.fileSizeSIUnits,
+ powered_on_hours_unit: this.poweredOnHoursUnit as DevicePoweredOnUnit,
line_stroke: this.lineStroke as LineStroke,
theme: this.theme as Theme,
metrics: {
diff --git a/webapp/frontend/src/app/modules/detail/detail.component.html b/webapp/frontend/src/app/modules/detail/detail.component.html
index 0f37778..13b19c2 100644
--- a/webapp/frontend/src/app/modules/detail/detail.component.html
+++ b/webapp/frontend/src/app/modules/detail/detail.component.html
@@ -123,7 +123,7 @@
Power Cycle Count
-
{{humanizeDuration(smart_results[0]?.power_on_hours *60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] })}}
+
{{ smart_results[0]?.power_on_hours | deviceHours:config.powered_on_hours_unit:{ round: true, largest: 1, units: ['y', 'd', 'h'] } }}
Powered On
diff --git a/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts b/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts
new file mode 100644
index 0000000..cabad90
--- /dev/null
+++ b/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts
@@ -0,0 +1,52 @@
+import { DeviceHoursPipe } from "./device-hours.pipe";
+
+describe("DeviceHoursPipe", () => {
+ it("create an instance", () => {
+ const pipe = new DeviceHoursPipe();
+ expect(pipe).toBeTruthy();
+ });
+
+ describe("#transform", () => {
+ const testCases = [
+ {
+ input: 12345,
+ configuration: "device_hours",
+ result: "12345 hours",
+ },
+ {
+ input: 15273,
+ configuration: "humanize",
+ result: "1 year, 8 months, 3 weeks, 6 days, 15 hours",
+ },
+ {
+ input: 48,
+ configuration: null,
+ result: "2 days",
+ },
+ {
+ input: 168,
+ configuration: "scrutiny",
+ result: "1 week",
+ },
+ {
+ input: null,
+ configuration: "device_hours",
+ result: "Unknown",
+ },
+ {
+ input: null,
+ configuration: "humanize",
+ result: "Unknown",
+ },
+ ];
+
+ testCases.forEach((test, index) => {
+ it(`format input '${test.input}' with configuration '${test.configuration}', should be '${test.result}' (testcase: ${index + 1})`, () => {
+ // test
+ const pipe = new DeviceHoursPipe();
+ const formatted = pipe.transform(test.input, test.configuration);
+ expect(formatted).toEqual(test.result);
+ });
+ });
+ });
+});
diff --git a/webapp/frontend/src/app/shared/device-hours.pipe.ts b/webapp/frontend/src/app/shared/device-hours.pipe.ts
new file mode 100644
index 0000000..1170c8a
--- /dev/null
+++ b/webapp/frontend/src/app/shared/device-hours.pipe.ts
@@ -0,0 +1,19 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import humanizeDuration from 'humanize-duration';
+
+@Pipe({ name: 'deviceHours' })
+export class DeviceHoursPipe implements PipeTransform {
+ static format(hoursOfRunTime: number, unit: string, humanizeConfig: object): string {
+ if (hoursOfRunTime === null) {
+ return 'Unknown';
+ }
+ if (unit === 'device_hours') {
+ return `${hoursOfRunTime} hours`;
+ }
+ return humanizeDuration(hoursOfRunTime * 60 * 60 * 1000, humanizeConfig);
+ }
+
+ transform(hoursOfRunTime: number, unit = 'humanize', humanizeConfig: any = {}): string {
+ return DeviceHoursPipe.format(hoursOfRunTime, unit, humanizeConfig)
+ }
+}
diff --git a/webapp/frontend/src/app/shared/shared.module.ts b/webapp/frontend/src/app/shared/shared.module.ts
index c22fdb2..1f9779b 100644
--- a/webapp/frontend/src/app/shared/shared.module.ts
+++ b/webapp/frontend/src/app/shared/shared.module.ts
@@ -6,6 +6,7 @@ import { DeviceSortPipe } from './device-sort.pipe';
import { TemperaturePipe } from './temperature.pipe';
import { DeviceTitlePipe } from './device-title.pipe';
import { DeviceStatusPipe } from './device-status.pipe';
+import { DeviceHoursPipe } from './device-hours.pipe';
@NgModule({
declarations: [
@@ -13,7 +14,8 @@ import { DeviceStatusPipe } from './device-status.pipe';
DeviceSortPipe,
TemperaturePipe,
DeviceTitlePipe,
- DeviceStatusPipe
+ DeviceStatusPipe,
+ DeviceHoursPipe
],
imports: [
CommonModule,
@@ -28,7 +30,8 @@ import { DeviceStatusPipe } from './device-status.pipe';
DeviceSortPipe,
DeviceTitlePipe,
DeviceStatusPipe,
- TemperaturePipe
+ TemperaturePipe,
+ DeviceHoursPipe
]
})
export class SharedModule