Skip to content
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

Add OMV integration / widget #1879

Merged
merged 6 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ NEXTAUTH_SECRET="anything"
# Disable analytics
NEXT_PUBLIC_DISABLE_ANALYTICS="true"

DEFAULT_COLOR_SCHEME="light"
DEFAULT_COLOR_SCHEME="light"
37 changes: 37 additions & 0 deletions public/locales/en/modules/health-monitoring.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"descriptor": {
"name": "System Health Monitoring",
"description": "Information about your NAS",
"settings": {
"title": "System Health Monitoring",
"fahrenheit": {
"label": "Fahrenheit"
}
}
},
"cpu": {
"label": "CPU",
"load": "Load Average",
"minute": "{{minute}} minute"
},
"memory": {
"label": "Memory",
"totalMem": "Total memory: {{total}}GB",
"available": "Available: {{available}}GB - {{percentage}}%"
},
"fileSystem": {
"label": "File System",
"available": "Available: {{available}} - {{percentage}}%"
},
"info": {
"uptime": "Uptime",
"updates": "Updates",
"reboot": "Reboot"
},
"errors": {
"general": {
"title": "Unable to find your NAS",
"text": "There was a problem connecting to your NAS. Please verify your configuration/integration(s)."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,9 @@ export const availableIntegrations = [
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png',
label: 'Home Assistant',
},
{
value: 'openmediavault',
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/openmediavault.png',
label: 'OpenMediaVault',
},
] as const satisfies Readonly<SelectItem[]>;
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { inviteRouter } from './routers/invite/invite-router';
import { mediaRequestsRouter } from './routers/media-request';
import { mediaServerRouter } from './routers/media-server';
import { notebookRouter } from './routers/notebook';
import { openmediavaultRouter } from './routers/openmediavault';
import { overseerrRouter } from './routers/overseerr';
import { passwordRouter } from './routers/password';
import { rssRouter } from './routers/rss';
Expand Down Expand Up @@ -49,6 +50,7 @@ export const rootRouter = createTRPCRouter({
password: passwordRouter,
notebook: notebookRouter,
smartHomeEntityState: smartHomeEntityStateRouter,
openmediavault: openmediavaultRouter,
});

// export type definition of API
Expand Down
119 changes: 119 additions & 0 deletions src/server/api/routers/openmediavault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import axios from 'axios';
import Consola from 'consola';
import { z } from 'zod';
import { checkIntegrationsType, findAppProperty } from '~/tools/client/app-properties';
import { getConfig } from '~/tools/config/getConfig';

import { createTRPCRouter, publicProcedure } from '../trpc';

let sessionId: string | null = null;
let loginToken: string | null = null;

async function makeOpenMediaVaultRPCCall(
serviceName: string,
method: string,
params: Record<string, any>,
headers: Record<string, string>,
input: { configName: string }
) {
const config = getConfig(input.configName);
const app = config.apps.find((app) => checkIntegrationsType(app.integration, ['openmediavault']));

if (!app) {
Consola.error(`App not found for configName '${input.configName}'`);
return null;
}

const appUrl = new URL(app.url);
const response = await axios.post(
`${appUrl.origin}/rpc.php`,
{
service: serviceName,
method: method,
params: params,
},
{
headers: {
'Content-Type': 'application/json',
...headers,
},
}
);
return response;
}

export const openmediavaultRouter = createTRPCRouter({
fetchData: publicProcedure
.input(
z.object({
configName: z.string(),
})
)
.query(async ({ input }) => {
let authResponse: any = null;
let app: any;

if (!sessionId || !loginToken) {
app = getConfig(input.configName)?.apps.find((app) =>
checkIntegrationsType(app.integration, ['openmediavault'])
);

if (!app) {
Consola.error(
`Failed to process request to app '${app.integration}' (${app.id}). Please check username & password`
);
return null;
}

authResponse = await makeOpenMediaVaultRPCCall(
'session',
'login',
{
username: findAppProperty(app, 'username'),
password: findAppProperty(app, 'password'),
},
{},
input
);

const cookies = authResponse.headers['set-cookie'] || [];
sessionId = cookies
.find((cookie: any) => cookie.includes('X-OPENMEDIAVAULT-SESSIONID'))
?.split(';')[0];
loginToken = cookies
.find((cookie: any) => cookie.includes('X-OPENMEDIAVAULT-LOGIN'))
?.split(';')[0];
}

const [systemInfoResponse, fileSystemResponse, cpuTempResponse] = await Promise.all([
makeOpenMediaVaultRPCCall(
'system',
'getInformation',
{},
{ Cookie: `${loginToken};${sessionId}` },
input
),
makeOpenMediaVaultRPCCall(
'filesystemmgmt',
'enumerateMountedFilesystems',
{ includeroot: true },
{ Cookie: `${loginToken};${sessionId}` },
input
),
makeOpenMediaVaultRPCCall(
'cputemp',
'get',
{},
{ Cookie: `${loginToken};${sessionId}` },
input
),
]);

return {
authenticated: authResponse ? authResponse.data.response.authenticated : true,
systemInfo: systemInfoResponse?.data.response,
fileSystem: fileSystemResponse?.data.response,
cpuTemp: cpuTempResponse?.data.response,
};
}),
});
1 change: 1 addition & 0 deletions src/tools/server/translation-namespaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const boardNamespaces = [
'modules/docker',
'modules/dashdot',
'modules/overseerr',
'modules/health-monitoring',
'modules/media-server',
'modules/indexer-manager',
'modules/common-media-cards',
Expand Down
4 changes: 3 additions & 1 deletion src/types/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export type IntegrationType =
| 'nzbGet'
| 'pihole'
| 'adGuardHome'
| 'homeAssistant';
| 'homeAssistant'
| 'openmediavault';

export type AppIntegrationType = {
type: IntegrationType | null;
Expand Down Expand Up @@ -101,6 +102,7 @@ export const integrationFieldProperties: {
pihole: ['apiKey'],
adGuardHome: ['username', 'password'],
homeAssistant: ['apiKey'],
openmediavault: ['username', 'password'],
};

export type IntegrationFieldDefinitionType = {
Expand Down
111 changes: 111 additions & 0 deletions src/widgets/health-monitoring/HealthMonitoringCpu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Center, Flex, Group, HoverCard, RingProgress, Text } from '@mantine/core';
import { IconCpu } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';

const HealthMonitoringCpu = ({ info, cpuTemp, fahrenheit }: any) => {
const { t } = useTranslation('modules/health-monitoring');
const toFahrenheit = (value: number) => {
return Math.round(value * 1.8 + 32);
};

interface LoadDataItem {
label: string;
stats: number;
progress: number;
color: string;
}

const loadData = [
{
label: `${t('cpu.minute', { minute: 1 })}`,
stats: info.loadAverage['1min'],
progress: info.loadAverage['1min'],
color: 'teal',
},
{
label: `${t('cpu.minute', { minute: 5 })}`,
stats: info.loadAverage['5min'],
progress: info.loadAverage['5min'],
color: 'blue',
},
{
label: `${t('cpu.minute', { minute: 15 })}`,
stats: info.loadAverage['15min'],
progress: info.loadAverage['15min'],
color: 'red',
},
] as const;

return (
<Group position="center">
<RingProgress
roundCaps
size={140}
thickness={12}
label={
<Center style={{ flexDirection: 'column' }}>
{info.cpuUtilization.toFixed(2)}%
<HoverCard width={280} shadow="md" position="top">
<HoverCard.Target>
<IconCpu size={40} />
</HoverCard.Target>
<HoverCard.Dropdown>
<Text fz="lg" tt="uppercase" fw={700} c="dimmed" align="center">
{t('cpu.load')}
</Text>
<Flex
direction={{ base: 'column', sm: 'row' }}
gap={{ base: 'sm', sm: 'lg' }}
justify={{ sm: 'center' }}
>
{loadData.map((load: LoadDataItem) => (
<RingProgress
size={80}
roundCaps
thickness={8}
label={
<Text color={load.color} weight={700} align="center" size="xl">
{load.progress}
</Text>
}
sections={[{ value: load.progress, color: load.color, tooltip: load.label }]}
/>
))}
</Flex>
</HoverCard.Dropdown>
</HoverCard>
</Center>
}
sections={[
{
value: info.cpuUtilization.toFixed(2),
color: info.cpuUtilization.toFixed(2) > 70 ? 'red' : 'green',
},
]}
/>
<RingProgress
roundCaps
size={140}
thickness={12}
label={
<Center
style={{
flexDirection: 'column',
}}
>
{fahrenheit ? `${toFahrenheit(cpuTemp.cputemp)}°F` : `${cpuTemp.cputemp}°C`}
<IconCpu size={40} />
</Center>
}
sections={[
{
value: cpuTemp.cputemp,
color: cpuTemp.cputemp < 60 ? 'green' : 'red',
},
]}
/>
</Group>
);
};

export default HealthMonitoringCpu;
62 changes: 62 additions & 0 deletions src/widgets/health-monitoring/HealthMonitoringFileSystem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Center, Flex, Group, HoverCard, RingProgress, Text } from '@mantine/core';
import { IconServer } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { humanFileSize } from '~/tools/humanFileSize';

import { ringColor } from './HealthMonitoringTile';

const HealthMonitoringFileSystem = ({ fileSystem }: any) => {
const { t } = useTranslation('modules/health-monitoring');

interface FileSystemDisk {
devicename: string;
used: string;
percentage: number;
available: number;
}

return (
<Group position="center">
<Flex
direction={{ base: 'column', sm: 'row' }}
gap={{ base: 'sm', sm: 'lg' }}
justify={{ sm: 'center' }}
>
{fileSystem.map((disk: FileSystemDisk) => (
<RingProgress
size={140}
roundCaps
thickness={12}
label={
<Center style={{ flexDirection: 'column' }}>
{disk.devicename}
<HoverCard width={280} shadow="md" position="top">
<HoverCard.Target>
<IconServer size={40} />
</HoverCard.Target>
<HoverCard.Dropdown>
<Text fz="lg" tt="uppercase" fw={700} c="dimmed" align="center">
{t('fileSystem.available', {
available: humanFileSize(disk.available),
percentage: 100 - disk.percentage,
})}
</Text>
</HoverCard.Dropdown>
</HoverCard>
</Center>
}
sections={[
{
value: disk.percentage,
color: ringColor(disk.percentage),
tooltip: disk.used,
},
]}
/>
))}
</Flex>
</Group>
);
};

export default HealthMonitoringFileSystem;
Loading
Loading