diff --git a/WebDisplay/.prettierrc b/WebDisplay/.prettierrc index cf63a71..2efac05 100644 --- a/WebDisplay/.prettierrc +++ b/WebDisplay/.prettierrc @@ -5,5 +5,7 @@ "semi": true, "singleQuote": true, "bracketSameLine": true, - "trailingComma": "none" + "trailingComma": "none", + "printWidth": 160, + "singleAttributePerLine": true } diff --git a/WebDisplay/README.md b/WebDisplay/README.md index 136a7ac..ef01e0a 100644 --- a/WebDisplay/README.md +++ b/WebDisplay/README.md @@ -41,7 +41,7 @@ This section covers how to start the development server and build your project f ### Starting the Development Server -To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000): +To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:4200](http://localhost:4200): ```bash yarn dev diff --git a/WebDisplay/components.d.ts b/WebDisplay/components.d.ts index 2911cff..a4684a4 100644 --- a/WebDisplay/components.d.ts +++ b/WebDisplay/components.d.ts @@ -13,8 +13,10 @@ declare module 'vue' { CurrentWeather: typeof import('./src/components/CurrentWeather.vue')['default'] DashboardItem: typeof import('./src/components/DashboardItem.vue')['default'] HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] + Indoor: typeof import('./src/components/Indoor.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + ValueChart: typeof import('./src/components/ValueChart.vue')['default'] WeatherSummary: typeof import('./src/components/WeatherSummary.vue')['default'] } } diff --git a/WebDisplay/package.json b/WebDisplay/package.json index a702ceb..6c88f6b 100644 --- a/WebDisplay/package.json +++ b/WebDisplay/package.json @@ -11,6 +11,7 @@ "@mdi/font": "7.0.96", "@microsoft/signalr": "^8.0.0", "@types/suncalc": "^1.9.2", + "apexcharts": "^3.46.0", "axios": "^1.6.7", "core-js": "^3.34.0", "date-fns": "^3.3.1", @@ -18,6 +19,7 @@ "roboto-fontface": "*", "suncalc": "^1.9.0", "vue": "^3.3.0", + "vue3-apexcharts": "^1.5.2", "vuetify": "^3.0.0" }, "devDependencies": { diff --git a/WebDisplay/pnpm-lock.yaml b/WebDisplay/pnpm-lock.yaml index f8b1579..603ad9d 100644 --- a/WebDisplay/pnpm-lock.yaml +++ b/WebDisplay/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@types/suncalc': specifier: ^1.9.2 version: 1.9.2 + apexcharts: + specifier: ^3.46.0 + version: 3.46.0 axios: specifier: ^1.6.7 version: 1.6.7 @@ -35,6 +38,9 @@ dependencies: vue: specifier: ^3.3.0 version: 3.4.21(typescript@5.3.3) + vue3-apexcharts: + specifier: ^1.5.2 + version: 1.5.2(apexcharts@3.46.0)(vue@3.4.21) vuetify: specifier: ^3.0.0 version: 3.5.6(typescript@5.3.3)(vite-plugin-vuetify@2.0.2)(vue@3.4.21) @@ -854,6 +860,10 @@ packages: vue: 3.4.21(typescript@5.3.3) vuetify: 3.5.6(typescript@5.3.3)(vite-plugin-vuetify@2.0.2)(vue@3.4.21) + /@yr/monotone-cubic-spline@1.0.3: + resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} + dev: false + /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -903,6 +913,18 @@ packages: normalize-path: 3.0.0 picomatch: 2.3.1 + /apexcharts@3.46.0: + resolution: {integrity: sha512-ELAY6vj8JQD7QLktKasTzwm9Wt0qxqfQSo+3QWS7G7I774iK8HCkG1toGsqJH0mkK6PtYBtnSIe66uUcwoCw1w==} + dependencies: + '@yr/monotone-cubic-spline': 1.0.3 + svg.draggable.js: 2.2.2 + svg.easing.js: 2.0.0 + svg.filter.js: 2.0.2 + svg.pathmorphing.js: 0.1.3 + svg.resize.js: 1.4.3 + svg.select.js: 3.0.1 + dev: false + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true @@ -2746,6 +2768,60 @@ packages: engines: {node: '>= 0.4'} dev: true + /svg.draggable.js@2.2.2: + resolution: {integrity: sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + + /svg.easing.js@2.0.0: + resolution: {integrity: sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + + /svg.filter.js@2.0.2: + resolution: {integrity: sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + + /svg.js@2.7.1: + resolution: {integrity: sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==} + dev: false + + /svg.pathmorphing.js@0.1.3: + resolution: {integrity: sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + + /svg.resize.js@1.4.3: + resolution: {integrity: sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + svg.select.js: 2.1.2 + dev: false + + /svg.select.js@2.1.2: + resolution: {integrity: sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + + /svg.select.js@3.0.1: + resolution: {integrity: sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -3088,6 +3164,16 @@ packages: typescript: 5.3.3 dev: true + /vue3-apexcharts@1.5.2(apexcharts@3.46.0)(vue@3.4.21): + resolution: {integrity: sha512-rGbgUJDjtsyjfRF0uzwDjzt8+M7ICSRAbm1N9KCDiczW8BSpbEZuaEsJDJYnJuLFIIVXIGilYzIcjNBf6NbeYA==} + peerDependencies: + apexcharts: '> 3.0.0' + vue: '> 3.0.0' + dependencies: + apexcharts: 3.46.0 + vue: 3.4.21(typescript@5.3.3) + dev: false + /vue@3.4.21(typescript@5.3.3): resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==} peerDependencies: diff --git a/WebDisplay/src/App.vue b/WebDisplay/src/App.vue index b7b4d9a..54d9778 100644 --- a/WebDisplay/src/App.vue +++ b/WebDisplay/src/App.vue @@ -11,13 +11,18 @@ + title="Outdoor" + to="outdoor"> + title="Indoor" + to="indoor"> + + diff --git a/WebDisplay/src/components/CurrentPower.vue b/WebDisplay/src/components/CurrentPower.vue index 608c58d..f4ddbe2 100644 --- a/WebDisplay/src/components/CurrentPower.vue +++ b/WebDisplay/src/components/CurrentPower.vue @@ -35,7 +35,7 @@ - diff --git a/WebDisplay/src/components/ValueChart.vue b/WebDisplay/src/components/ValueChart.vue new file mode 100644 index 0000000..afea7bc --- /dev/null +++ b/WebDisplay/src/components/ValueChart.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/WebDisplay/src/components/WeatherSummary.vue b/WebDisplay/src/components/WeatherSummary.vue index 70aae1e..32486c0 100644 --- a/WebDisplay/src/components/WeatherSummary.vue +++ b/WebDisplay/src/components/WeatherSummary.vue @@ -3,6 +3,7 @@ import { useWeatherStore } from '@/stores/weatherStore'; import { subHours } from 'date-fns'; import { WeatherAggregates } from '@/models/weather/weather-aggregates'; + import { ConvertPascalToInchesOfMercury } from '@/pressureConverter'; const weatherAggregates = ref(); @@ -11,11 +12,9 @@ const end = new Date(); const start = subHours(end, 24); - weatherStore - .getReadingAggregate(start, end) - .then((newWeatherAggregates) => { - weatherAggregates.value = newWeatherAggregates; - }); + weatherStore.getReadingAggregate(start, end).then((newWeatherAggregates) => { + weatherAggregates.value = newWeatherAggregates; + }); diff --git a/WebDisplay/src/pages/indoor.vue b/WebDisplay/src/pages/indoor.vue new file mode 100644 index 0000000..5c9c163 --- /dev/null +++ b/WebDisplay/src/pages/indoor.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/WebDisplay/src/pages/inside.vue b/WebDisplay/src/pages/inside.vue deleted file mode 100644 index c731142..0000000 --- a/WebDisplay/src/pages/inside.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/WebDisplay/src/pages/outdoor.vue b/WebDisplay/src/pages/outdoor.vue new file mode 100644 index 0000000..dfbcaa2 --- /dev/null +++ b/WebDisplay/src/pages/outdoor.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/WebDisplay/src/pages/outside.vue b/WebDisplay/src/pages/outside.vue deleted file mode 100644 index 72467af..0000000 --- a/WebDisplay/src/pages/outside.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/WebDisplay/src/pages/power.vue b/WebDisplay/src/pages/power.vue new file mode 100644 index 0000000..7323aa4 --- /dev/null +++ b/WebDisplay/src/pages/power.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/WebDisplay/src/pressureConverter.ts b/WebDisplay/src/pressureConverter.ts new file mode 100644 index 0000000..1605bc4 --- /dev/null +++ b/WebDisplay/src/pressureConverter.ts @@ -0,0 +1,7 @@ +export function ConvertMillibarToInchesOfMercury(value: number): number { + return value / 33.864; +} + +export function ConvertPascalToInchesOfMercury(value: number): number { + return value / 33.864 / 100.0; +} diff --git a/WebDisplay/src/stores/indoorStore.ts b/WebDisplay/src/stores/indoorStore.ts new file mode 100644 index 0000000..bb351cc --- /dev/null +++ b/WebDisplay/src/stores/indoorStore.ts @@ -0,0 +1,62 @@ +import { defineStore } from 'pinia'; +import axios from 'axios'; +import Environment from '@/environment'; +import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; +import { LatestReadings } from '@/models/environment.ts/latestReadings'; +import { ReadingsGrouped } from '@/models/environment.ts/readingsGrouped'; + +export function createIndoorStore(name: string) { + return defineStore(`indoor-${name}`, { + state: () => { + return { + current: null as LatestReadings | null, + _connection: null as HubConnection | null + }; + }, + actions: { + async start() { + if (this._connection) { + return; + } + + this._connection = new HubConnectionBuilder() + .withUrl(Environment.getUrlPrefix() + '/api/hub/environment', { + withCredentials: false + }) + .build(); + + await this._connection.start(); + + this._connection.on('Latest', (message: string) => { + const latestReadings = JSON.parse(message) as LatestReadings; + + if (latestReadings.name === name) { + this.$patch({ current: latestReadings }); + } + }); + + this._connection.send('RequestLatest'); + }, + async stop() { + if (!this._connection) { + return; + } + + await this._connection.stop(); + + this._connection = null; + }, + async getReadingValueHistoryGrouped(start: Date, end: Date, bucketMinutes: number): Promise { + const startString = start.toISOString(); + const endString = end.toISOString(); + + const response = await axios.get( + Environment.getUrlPrefix() + + `/api/environment/readings/history-grouped?start=${startString}&end=${endString}&bucketMinutes=${bucketMinutes}` + ); + + return response.data; + } + } + })(); +} diff --git a/WebDisplay/src/stores/powerStore.ts b/WebDisplay/src/stores/powerStore.ts index 885e844..8e48029 100644 --- a/WebDisplay/src/stores/powerStore.ts +++ b/WebDisplay/src/stores/powerStore.ts @@ -1,7 +1,9 @@ import { defineStore } from 'pinia'; import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; +import axios from 'axios'; import Environment from '@/environment'; import PowerStatus from '@/models/power/power-status'; +import PowerHistoryGrouped from '@/models/power/power-history-grouped'; export const usePowerStore = defineStore('power', { state: () => { @@ -36,6 +38,16 @@ export const usePowerStore = defineStore('power', { await this._connection.stop(); this._connection = null; + }, + async getReadingHistoryGrouped(start: Date, end: Date, bucketMinutes: number): Promise { + const startString = start.toISOString(); + const endString = end.toISOString(); + + const response = await axios.get( + Environment.getUrlPrefix() + `/api/power/status/history-grouped?start=${startString}&end=${endString}&bucketMinutes=${bucketMinutes}` + ); + + return response.data; } } }); diff --git a/WebDisplay/src/stores/weatherStore.ts b/WebDisplay/src/stores/weatherStore.ts index 589ed10..30b2cda 100644 --- a/WebDisplay/src/stores/weatherStore.ts +++ b/WebDisplay/src/stores/weatherStore.ts @@ -6,7 +6,9 @@ import WeatherUpdate from '@/models/weather/weather-update'; import WeatherRecent from '@/models/weather/weather-recent'; import WeatherValueType from '@/models/weather/weather-value-type'; import WeatherValueGrouped from '@/models/weather/weather-value-grouped'; -import { WeatherAggregates } from '@/models/weather/weather-aggregates'; +import WeatherHistoryGrouped from '@/models/weather/weather-history-grouped'; +import WindHistoryGrouped from '@/models/weather/wind-history-grouped'; +import WeatherAggregates from '@/models/weather/weather-aggregates'; export const useWeatherStore = defineStore('weather', { state: () => { @@ -43,18 +45,11 @@ export const useWeatherStore = defineStore('weather', { this._connection = null; }, async getLatest(): Promise { - const response = await axios.get( - Environment.getUrlPrefix() + `/api/weather/readings/recent` - ); + const response = await axios.get(Environment.getUrlPrefix() + `/api/weather/readings/recent`); return response.data; }, - async getReadingValueHistoryGrouped( - valueType: WeatherValueType, - start: Date, - end: Date, - bucketMinutes: number - ): Promise { + async getReadingValueHistoryGrouped(valueType: WeatherValueType, start: Date, end: Date, bucketMinutes: number): Promise { const startString = start.toISOString(); const endString = end.toISOString(); @@ -65,16 +60,32 @@ export const useWeatherStore = defineStore('weather', { return response.data; }, - async getReadingAggregate( - start: Date, - end: Date - ): Promise { + async getReadingAggregate(start: Date, end: Date): Promise { const startString = start.toISOString(); const endString = end.toISOString(); const response = await axios.get( - Environment.getUrlPrefix() + - `/api/weather/readings/aggregate?start=${startString}&end=${endString}` + Environment.getUrlPrefix() + `/api/weather/readings/aggregate?start=${startString}&end=${endString}` + ); + + return response.data; + }, + async getReadingHistoryGrouped(start: Date, end: Date, bucketMinutes: number): Promise { + const startString = start.toISOString(); + const endString = end.toISOString(); + + const response = await axios.get( + Environment.getUrlPrefix() + `/api/weather/readings/history-grouped?start=${startString}&end=${endString}&bucketMinutes=${bucketMinutes}` + ); + + return response.data; + }, + async getWindHistoryGrouped(start: Date, end: Date, bucketMinutes: number): Promise { + const startString = start.toISOString(); + const endString = end.toISOString(); + + const response = await axios.get( + Environment.getUrlPrefix() + `/api/weather/readings/wind-history-grouped?start=${startString}&end=${endString}&bucketMinutes=${bucketMinutes}` ); return response.data; diff --git a/WebDisplay/src/temperatureConverter.ts b/WebDisplay/src/temperatureConverter.ts new file mode 100644 index 0000000..23419d9 --- /dev/null +++ b/WebDisplay/src/temperatureConverter.ts @@ -0,0 +1,3 @@ +export function ConvertCToF(value: number): number { + return (value * 9.0) / 5.0 + 32.0; +} diff --git a/WebDisplay/typed-router.d.ts b/WebDisplay/typed-router.d.ts index 2463b8d..57aac90 100644 --- a/WebDisplay/typed-router.d.ts +++ b/WebDisplay/typed-router.d.ts @@ -40,8 +40,9 @@ import type { declare module 'vue-router/auto/routes' { export interface RouteNamedMap { '/': RouteRecordInfo<'/', '/', Record, Record>, - '/inside': RouteRecordInfo<'/inside', '/inside', Record, Record>, - '/outside': RouteRecordInfo<'/outside', '/outside', Record, Record>, + '/indoor': RouteRecordInfo<'/indoor', '/indoor', Record, Record>, + '/outdoor': RouteRecordInfo<'/outdoor', '/outdoor', Record, Record>, + '/power': RouteRecordInfo<'/power', '/power', Record, Record>, } } diff --git a/WebDisplay/vite.config.mts b/WebDisplay/vite.config.mts index 8ab5750..43e6cec 100644 --- a/WebDisplay/vite.config.mts +++ b/WebDisplay/vite.config.mts @@ -43,6 +43,6 @@ export default defineConfig({ extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'], }, server: { - port: 3000, + port: 4200, }, });