mirror of
https://github.com/ckaczor/HomeMonitor.git
synced 2026-02-16 10:58:32 -05:00
Bunch more new UI
This commit is contained in:
@@ -5,5 +5,7 @@
|
|||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"bracketSameLine": true,
|
"bracketSameLine": true,
|
||||||
"trailingComma": "none"
|
"trailingComma": "none",
|
||||||
|
"printWidth": 160,
|
||||||
|
"singleAttributePerLine": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ This section covers how to start the development server and build your project f
|
|||||||
|
|
||||||
### Starting the Development Server
|
### 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
|
```bash
|
||||||
yarn dev
|
yarn dev
|
||||||
|
|||||||
2
WebDisplay/components.d.ts
vendored
2
WebDisplay/components.d.ts
vendored
@@ -13,8 +13,10 @@ declare module 'vue' {
|
|||||||
CurrentWeather: typeof import('./src/components/CurrentWeather.vue')['default']
|
CurrentWeather: typeof import('./src/components/CurrentWeather.vue')['default']
|
||||||
DashboardItem: typeof import('./src/components/DashboardItem.vue')['default']
|
DashboardItem: typeof import('./src/components/DashboardItem.vue')['default']
|
||||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||||
|
Indoor: typeof import('./src/components/Indoor.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
ValueChart: typeof import('./src/components/ValueChart.vue')['default']
|
||||||
WeatherSummary: typeof import('./src/components/WeatherSummary.vue')['default']
|
WeatherSummary: typeof import('./src/components/WeatherSummary.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"@mdi/font": "7.0.96",
|
"@mdi/font": "7.0.96",
|
||||||
"@microsoft/signalr": "^8.0.0",
|
"@microsoft/signalr": "^8.0.0",
|
||||||
"@types/suncalc": "^1.9.2",
|
"@types/suncalc": "^1.9.2",
|
||||||
|
"apexcharts": "^3.46.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"core-js": "^3.34.0",
|
"core-js": "^3.34.0",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
"suncalc": "^1.9.0",
|
"suncalc": "^1.9.0",
|
||||||
"vue": "^3.3.0",
|
"vue": "^3.3.0",
|
||||||
|
"vue3-apexcharts": "^1.5.2",
|
||||||
"vuetify": "^3.0.0"
|
"vuetify": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
86
WebDisplay/pnpm-lock.yaml
generated
86
WebDisplay/pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ dependencies:
|
|||||||
'@types/suncalc':
|
'@types/suncalc':
|
||||||
specifier: ^1.9.2
|
specifier: ^1.9.2
|
||||||
version: 1.9.2
|
version: 1.9.2
|
||||||
|
apexcharts:
|
||||||
|
specifier: ^3.46.0
|
||||||
|
version: 3.46.0
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.6.7
|
specifier: ^1.6.7
|
||||||
version: 1.6.7
|
version: 1.6.7
|
||||||
@@ -35,6 +38,9 @@ dependencies:
|
|||||||
vue:
|
vue:
|
||||||
specifier: ^3.3.0
|
specifier: ^3.3.0
|
||||||
version: 3.4.21(typescript@5.3.3)
|
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:
|
vuetify:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.5.6(typescript@5.3.3)(vite-plugin-vuetify@2.0.2)(vue@3.4.21)
|
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)
|
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)
|
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:
|
/abort-controller@3.0.0:
|
||||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
engines: {node: '>=6.5'}
|
engines: {node: '>=6.5'}
|
||||||
@@ -903,6 +913,18 @@ packages:
|
|||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
picomatch: 2.3.1
|
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:
|
/argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -2746,6 +2768,60 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dev: true
|
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:
|
/text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -3088,6 +3164,16 @@ packages:
|
|||||||
typescript: 5.3.3
|
typescript: 5.3.3
|
||||||
dev: true
|
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):
|
/vue@3.4.21(typescript@5.3.3):
|
||||||
resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==}
|
resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
@@ -11,13 +11,18 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
prepend-icon="mdi-sun-thermometer"
|
prepend-icon="mdi-sun-thermometer"
|
||||||
title="Outside"
|
title="Outdoor"
|
||||||
to="outside">
|
to="outdoor">
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
prepend-icon="mdi-home-analytics"
|
prepend-icon="mdi-home-analytics"
|
||||||
title="Inside"
|
title="Indoor"
|
||||||
to="inside">
|
to="indoor">
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-home-lightning-bolt-outline"
|
||||||
|
title="Power"
|
||||||
|
to="power">
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
</DashboardItem>
|
</DashboardItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.power-current {
|
.power-current {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useWeatherStore } from '@/stores/weatherStore';
|
import { useWeatherStore } from '@/stores/weatherStore';
|
||||||
|
import { ConvertPascalToInchesOfMercury } from '@/pressureConverter';
|
||||||
|
|
||||||
const weatherStore = useWeatherStore();
|
const weatherStore = useWeatherStore();
|
||||||
weatherStore.start();
|
weatherStore.start();
|
||||||
@@ -31,62 +32,33 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="weather-current-header">Temperature</td>
|
<td className="weather-current-header">Temperature</td>
|
||||||
<td>
|
<td>{{ weatherStore.current?.Temperature?.toFixed(2) }}°F</td>
|
||||||
{{
|
|
||||||
weatherStore.current?.Temperature?.toFixed(2)
|
|
||||||
}}°F
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="weatherStore.current?.HeatIndex">
|
<tr v-if="weatherStore.current?.HeatIndex">
|
||||||
<td className="weather-current-header">Heat index</td>
|
<td className="weather-current-header">Heat index</td>
|
||||||
<td>
|
<td>{{ weatherStore.current?.HeatIndex?.toFixed(2) }}°F</td>
|
||||||
{{ weatherStore.current?.HeatIndex?.toFixed(2) }}°F
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr v-if="weatherStore.current?.WindChill">
|
<tr v-if="weatherStore.current?.WindChill">
|
||||||
<td className="weather-current-header">Wind chill</td>
|
<td className="weather-current-header">Wind chill</td>
|
||||||
<td>
|
<td>{{ weatherStore.current?.WindChill?.toFixed(2) }}°F</td>
|
||||||
{{ weatherStore.current?.WindChill?.toFixed(2) }}°F
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="weather-current-header">Humidity</td>
|
<td className="weather-current-header">Humidity</td>
|
||||||
<td>
|
<td>{{ weatherStore.current?.Humidity?.toFixed(2) }}%</td>
|
||||||
{{ weatherStore.current?.Humidity?.toFixed(2) }}%
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="weather-current-header">Dew point</td>
|
<td className="weather-current-header">Dew point</td>
|
||||||
<td>
|
<td>{{ weatherStore.current?.DewPoint?.toFixed(2) }}°F</td>
|
||||||
{{ weatherStore.current?.DewPoint?.toFixed(2) }}°F
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="weather-current-header">Pressure</td>
|
<td className="weather-current-header">Pressure</td>
|
||||||
<td>
|
<td>
|
||||||
{{
|
{{ weatherStore.current?.Pressure && ConvertPascalToInchesOfMercury(weatherStore.current?.Pressure)?.toFixed(2) }}"
|
||||||
weatherStore.current?.Pressure &&
|
|
||||||
(
|
|
||||||
weatherStore.current?.Pressure /
|
|
||||||
33.864 /
|
|
||||||
100
|
|
||||||
)?.toFixed(2)
|
|
||||||
}}"
|
|
||||||
<span
|
<span
|
||||||
class="pressure-trend-arrow"
|
class="pressure-trend-arrow"
|
||||||
:class="
|
:class="rotationClass(weatherStore.current?.PressureDifferenceThreeHour)"
|
||||||
rotationClass(
|
:title="'3 Hour Change: ' + weatherStore.current?.PressureDifferenceThreeHour?.toFixed(1)">
|
||||||
weatherStore.current
|
|
||||||
?.PressureDifferenceThreeHour
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:title="
|
|
||||||
'3 Hour Change: ' +
|
|
||||||
weatherStore.current?.PressureDifferenceThreeHour?.toFixed(
|
|
||||||
1
|
|
||||||
)
|
|
||||||
">
|
|
||||||
➜
|
➜
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -100,11 +72,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="weather-current-header">Rain</td>
|
<td className="weather-current-header">Rain</td>
|
||||||
<td>
|
<td>{{ weatherStore.current?.RainLastHour?.toFixed(2) }}" (last hour)</td>
|
||||||
{{
|
|
||||||
weatherStore.current?.RainLastHour?.toFixed(2)
|
|
||||||
}}" (last hour)
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="weather-current-header">Light</td>
|
<td className="weather-current-header">Light</td>
|
||||||
|
|||||||
120
WebDisplay/src/components/Indoor.vue
Normal file
120
WebDisplay/src/components/Indoor.vue
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { createIndoorStore } from '@/stores/indoorStore';
|
||||||
|
import { ConvertCToF } from '@/temperatureConverter';
|
||||||
|
import { ConvertMillibarToInchesOfMercury } from '@/pressureConverter';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
deviceName: { type: String, required: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const indoorStore = createIndoorStore(props.deviceName);
|
||||||
|
indoorStore.start();
|
||||||
|
|
||||||
|
const airQualityDescription = (airQualityIndex: number | undefined) => {
|
||||||
|
if (!airQualityIndex) {
|
||||||
|
return '';
|
||||||
|
} else if (airQualityIndex >= 0 && airQualityIndex <= 50) {
|
||||||
|
return 'Good';
|
||||||
|
} else if (airQualityIndex >= 51 && airQualityIndex <= 100) {
|
||||||
|
return 'Moderate';
|
||||||
|
} else if (airQualityIndex >= 101 && airQualityIndex <= 150) {
|
||||||
|
return 'Unhealthy for Sensitive Groups';
|
||||||
|
} else if (airQualityIndex >= 151 && airQualityIndex <= 200) {
|
||||||
|
return 'Unhealthy';
|
||||||
|
} else if (airQualityIndex >= 201 && airQualityIndex <= 300) {
|
||||||
|
return 'Very Unhealthy';
|
||||||
|
} else if (airQualityIndex >= 301) {
|
||||||
|
return 'Hazardous';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const airQualityClass = (airQualityIndex: number | undefined) => {
|
||||||
|
if (!airQualityIndex) {
|
||||||
|
return 'aqi-none';
|
||||||
|
} else if (airQualityIndex >= 0 && airQualityIndex <= 50) {
|
||||||
|
return 'aqi-green';
|
||||||
|
} else if (airQualityIndex >= 51 && airQualityIndex <= 100) {
|
||||||
|
return 'aqi-yellow';
|
||||||
|
} else if (airQualityIndex >= 101 && airQualityIndex <= 150) {
|
||||||
|
return 'aqi-orange';
|
||||||
|
} else if (airQualityIndex >= 151 && airQualityIndex <= 200) {
|
||||||
|
return 'aqi-red';
|
||||||
|
} else if (airQualityIndex >= 201 && airQualityIndex <= 300) {
|
||||||
|
return 'aqi-purple';
|
||||||
|
} else if (airQualityIndex >= 301) {
|
||||||
|
return 'aqi-maroon';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DashboardItem :title="title">
|
||||||
|
<div className="current">
|
||||||
|
<div v-if="!indoorStore.current">Loading...</div>
|
||||||
|
<table v-else>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="header">Temperature</td>
|
||||||
|
<td>{{ ConvertCToF(indoorStore.current.temperature).toFixed(2) }}°F</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="header">Humidity</td>
|
||||||
|
<td>{{ indoorStore.current.humidity.toFixed(2) }}%</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="header">Pressure</td>
|
||||||
|
<td>{{ ConvertMillibarToInchesOfMercury(indoorStore.current.pressure).toFixed(2) }}"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="header">Air Quality</td>
|
||||||
|
<td :class="airQualityClass(indoorStore.current.airQualityIndex)" :title="indoorStore.current.airQualityIndex.toString()">
|
||||||
|
{{ airQualityDescription(indoorStore.current.airQualityIndex) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</DashboardItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.current {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aqi-green {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aqi-yellow {
|
||||||
|
color: goldenrod;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aqi-orange {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aqi-red {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aqi-purple {
|
||||||
|
color: purple;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aqi-maroon {
|
||||||
|
color: maroon;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
109
WebDisplay/src/components/ValueChart.vue
Normal file
109
WebDisplay/src/components/ValueChart.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ApexOptions } from 'apexcharts';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: { type: String, required: true },
|
||||||
|
title: { type: String, required: true },
|
||||||
|
unit: { type: String, required: true },
|
||||||
|
categories: { type: Array<number>, required: true },
|
||||||
|
series: { type: Array<{ name: string; data: Array<number> }>, required: true },
|
||||||
|
yAxisDecimalPoints: { type: Number, required: false, default: 0 },
|
||||||
|
valueDecimalPoints: { type: Number, required: false, default: 2 },
|
||||||
|
group: { type: String, required: false, default: undefined },
|
||||||
|
stepline: { type: Boolean, required: false, default: false },
|
||||||
|
ready: { type: Boolean, required: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
var chartOptions: ApexOptions = {
|
||||||
|
chart: {
|
||||||
|
id: props.title,
|
||||||
|
animations: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
group: props.group,
|
||||||
|
toolbar: {
|
||||||
|
tools: {
|
||||||
|
pan: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
text: props.title,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
itemMargin: {
|
||||||
|
vertical: 5
|
||||||
|
},
|
||||||
|
showForSingleSeries: true
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
x: {
|
||||||
|
format: 'MMMM d h:mm TT'
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
formatter: (value) => {
|
||||||
|
return `${value.toFixed(props.valueDecimalPoints)}${props.unit}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
type: 'datetime',
|
||||||
|
categories: props.categories,
|
||||||
|
tooltip: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
datetimeUTC: false,
|
||||||
|
datetimeFormatter: {
|
||||||
|
day: 'MMM d',
|
||||||
|
hour: 'h:mm TT'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
formatter: (value) => {
|
||||||
|
return `${value.toFixed(props.yAxisDecimalPoints)}${props.unit}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
width: 2,
|
||||||
|
curve: props.stepline ? 'stepline' : 'smooth'
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var chartSeries: ApexAxisChartSeries = props.series;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container
|
||||||
|
v-if="!props.ready"
|
||||||
|
class="fill-height loading">
|
||||||
|
<v-responsive class="align-center text-center fill-height">
|
||||||
|
<v-progress-circular
|
||||||
|
:size="50"
|
||||||
|
:width="5"
|
||||||
|
color="primary"
|
||||||
|
indeterminate></v-progress-circular>
|
||||||
|
</v-responsive>
|
||||||
|
</v-container>
|
||||||
|
<apexchart
|
||||||
|
v-else
|
||||||
|
width="100%"
|
||||||
|
height="300"
|
||||||
|
:type="props.type"
|
||||||
|
:options="chartOptions"
|
||||||
|
:series="chartSeries"></apexchart>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useWeatherStore } from '@/stores/weatherStore';
|
import { useWeatherStore } from '@/stores/weatherStore';
|
||||||
import { subHours } from 'date-fns';
|
import { subHours } from 'date-fns';
|
||||||
import { WeatherAggregates } from '@/models/weather/weather-aggregates';
|
import { WeatherAggregates } from '@/models/weather/weather-aggregates';
|
||||||
|
import { ConvertPascalToInchesOfMercury } from '@/pressureConverter';
|
||||||
|
|
||||||
const weatherAggregates = ref<WeatherAggregates | undefined>();
|
const weatherAggregates = ref<WeatherAggregates | undefined>();
|
||||||
|
|
||||||
@@ -11,9 +12,7 @@
|
|||||||
const end = new Date();
|
const end = new Date();
|
||||||
const start = subHours(end, 24);
|
const start = subHours(end, 24);
|
||||||
|
|
||||||
weatherStore
|
weatherStore.getReadingAggregate(start, end).then((newWeatherAggregates) => {
|
||||||
.getReadingAggregate(start, end)
|
|
||||||
.then((newWeatherAggregates) => {
|
|
||||||
weatherAggregates.value = newWeatherAggregates;
|
weatherAggregates.value = newWeatherAggregates;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -32,78 +31,35 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="weather-summary-header">Temperature</td>
|
<td class="weather-summary-header">Temperature</td>
|
||||||
<td>
|
<td>{{ weatherAggregates!.temperature.min.toFixed(2) }}°F</td>
|
||||||
{{
|
<td>{{ weatherAggregates!.temperature.average.toFixed(2) }}°F</td>
|
||||||
weatherAggregates!.temperature.min.toFixed(2)
|
<td>{{ weatherAggregates!.temperature.max.toFixed(2) }}°F</td>
|
||||||
}}°F
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{
|
|
||||||
weatherAggregates!.temperature.average.toFixed(
|
|
||||||
2
|
|
||||||
)
|
|
||||||
}}°F
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{
|
|
||||||
weatherAggregates!.temperature.max.toFixed(2)
|
|
||||||
}}°F
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="weather-summary-header">Humidity</td>
|
<td class="weather-summary-header">Humidity</td>
|
||||||
<td>
|
<td>{{ weatherAggregates!.humidity.min.toFixed(2) }}%</td>
|
||||||
{{ weatherAggregates!.humidity.min.toFixed(2) }}%
|
<td>{{ weatherAggregates!.humidity.average.toFixed(2) }}%</td>
|
||||||
</td>
|
<td>{{ weatherAggregates!.humidity.max.toFixed(2) }}%</td>
|
||||||
<td>
|
|
||||||
{{
|
|
||||||
weatherAggregates!.humidity.average.toFixed(2)
|
|
||||||
}}%
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ weatherAggregates!.humidity.max.toFixed(2) }}%
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="weather-summary-header">Pressure</td>
|
<td class="weather-summary-header">Pressure</td>
|
||||||
<td>
|
<td>{{ ConvertPascalToInchesOfMercury(weatherAggregates!.pressure.min).toFixed(2) }}"</td>
|
||||||
{{
|
<td>{{ ConvertPascalToInchesOfMercury(weatherAggregates!.pressure.average).toFixed(2) }}"</td>
|
||||||
(
|
<td>{{ ConvertPascalToInchesOfMercury(weatherAggregates!.pressure.max).toFixed(2) }}"</td>
|
||||||
weatherAggregates!.pressure.min /
|
|
||||||
33.864 /
|
|
||||||
100
|
|
||||||
).toFixed(2)
|
|
||||||
}}"
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{
|
|
||||||
(
|
|
||||||
weatherAggregates!.pressure.average /
|
|
||||||
33.864 /
|
|
||||||
100
|
|
||||||
).toFixed(2)
|
|
||||||
}}"
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{
|
|
||||||
(
|
|
||||||
weatherAggregates!.pressure.max /
|
|
||||||
33.864 /
|
|
||||||
100
|
|
||||||
).toFixed(2)
|
|
||||||
}}"
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="weather-summary-header">Light</td>
|
<td class="weather-summary-header">Light</td>
|
||||||
<td>
|
<td>
|
||||||
{{ weatherAggregates!.light.min.toFixed(2) }} lx
|
{{ weatherAggregates!.light.min.toFixed(2) }}
|
||||||
|
lx
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ weatherAggregates!.light.average.toFixed(2) }} lx
|
{{ weatherAggregates!.light.average.toFixed(2) }}
|
||||||
|
lx
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ weatherAggregates!.light.max.toFixed(2) }} lx
|
{{ weatherAggregates!.light.max.toFixed(2) }}
|
||||||
|
lx
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -113,9 +69,7 @@
|
|||||||
mph
|
mph
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{
|
{{ weatherAggregates!.windSpeed.average.toFixed(2) }}
|
||||||
weatherAggregates!.windSpeed.average.toFixed(2)
|
|
||||||
}}
|
|
||||||
mph
|
mph
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import App from './App.vue';
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
|
import VueApexCharts from 'vue3-apexcharts';
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
registerPlugins(app);
|
registerPlugins(app);
|
||||||
|
|
||||||
|
app.use(VueApexCharts);
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|||||||
13
WebDisplay/src/models/environment.ts/latestReadings.ts
Normal file
13
WebDisplay/src/models/environment.ts/latestReadings.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export interface LatestReadings {
|
||||||
|
time: string
|
||||||
|
name: string
|
||||||
|
model: string
|
||||||
|
airQualityIndex: number
|
||||||
|
colorTemperature: number
|
||||||
|
gasResistance: number
|
||||||
|
humidity: number
|
||||||
|
luminance: number
|
||||||
|
pressure: number
|
||||||
|
temperature: number
|
||||||
|
}
|
||||||
|
|
||||||
11
WebDisplay/src/models/environment.ts/readingsGrouped.ts
Normal file
11
WebDisplay/src/models/environment.ts/readingsGrouped.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface ReadingsGrouped {
|
||||||
|
bucket: string;
|
||||||
|
name: string;
|
||||||
|
averageTemperature: number;
|
||||||
|
averagePressure: number;
|
||||||
|
averageHumidity: number;
|
||||||
|
averageLuminance: number;
|
||||||
|
averageGasResistance: number;
|
||||||
|
averageColorTemperature: number;
|
||||||
|
averageAirQualityIndex: number;
|
||||||
|
}
|
||||||
5
WebDisplay/src/models/power/power-history-grouped.ts
Normal file
5
WebDisplay/src/models/power/power-history-grouped.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default interface PowerHistoryGrouped {
|
||||||
|
bucket: string;
|
||||||
|
averageGeneration: number;
|
||||||
|
averageConsumption: number;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface WeatherAggregates {
|
export default interface WeatherAggregates {
|
||||||
humidity: WeatherAggregate;
|
humidity: WeatherAggregate;
|
||||||
temperature: WeatherAggregate;
|
temperature: WeatherAggregate;
|
||||||
pressure: WeatherAggregate;
|
pressure: WeatherAggregate;
|
||||||
|
|||||||
8
WebDisplay/src/models/weather/weather-history-grouped.ts
Normal file
8
WebDisplay/src/models/weather/weather-history-grouped.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default interface WeatherHistoryGrouped {
|
||||||
|
bucket: string;
|
||||||
|
averageTemperature: number;
|
||||||
|
averagePressure: number;
|
||||||
|
averageLightLevel: number;
|
||||||
|
averageHumidity: number;
|
||||||
|
rainTotal: number;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export default class WeatherValueGrouped {
|
export default interface WeatherValueGrouped {
|
||||||
bucket: string | undefined;
|
bucket: string;
|
||||||
averageValue: number | undefined;
|
averageValue: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ export enum WeatherValueType {
|
|||||||
Humidity = 'Humidity',
|
Humidity = 'Humidity',
|
||||||
HumidityTemperature = 'HumidityTemperature',
|
HumidityTemperature = 'HumidityTemperature',
|
||||||
PressureTemperature = 'PressureTemperature',
|
PressureTemperature = 'PressureTemperature',
|
||||||
Pressure = 'Pressure'
|
Pressure = 'Pressure',
|
||||||
|
Light = 'LightLevel'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WeatherValueType;
|
export default WeatherValueType;
|
||||||
|
|||||||
7
WebDisplay/src/models/weather/wind-history-grouped.ts
Normal file
7
WebDisplay/src/models/weather/wind-history-grouped.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default interface WindHistoryGrouped {
|
||||||
|
bucket: string;
|
||||||
|
minimumSpeed: number;
|
||||||
|
averageSpeed: number;
|
||||||
|
maximumSpeed: number;
|
||||||
|
averageDirection: number;
|
||||||
|
}
|
||||||
@@ -23,6 +23,12 @@
|
|||||||
<v-col cols="4">
|
<v-col cols="4">
|
||||||
<WeatherSummary></WeatherSummary>
|
<WeatherSummary></WeatherSummary>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
<v-col cols="2">
|
||||||
|
<Indoor title="Upstairs" deviceName="main"></Indoor>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="2">
|
||||||
|
<Indoor title="Downstairs" deviceName="basement"></Indoor>
|
||||||
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
99
WebDisplay/src/pages/indoor.vue
Normal file
99
WebDisplay/src/pages/indoor.vue
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { subHours } from 'date-fns';
|
||||||
|
import { createIndoorStore } from '@/stores/indoorStore';
|
||||||
|
import { ConvertCToF } from '@/temperatureConverter';
|
||||||
|
import { ConvertMillibarToInchesOfMercury } from '@/pressureConverter';
|
||||||
|
import ValueChart from '../components/ValueChart.vue';
|
||||||
|
|
||||||
|
const indoorStore = createIndoorStore('charts');
|
||||||
|
|
||||||
|
const ready = ref(false);
|
||||||
|
|
||||||
|
const categories: number[] = [];
|
||||||
|
|
||||||
|
const mainTemperatureSeries = { name: 'Upstairs', data: [] as number[] };
|
||||||
|
const basementTemperatureSeries = { name: 'Downstairs', data: [] as number[] };
|
||||||
|
|
||||||
|
const mainHumiditySeries = { name: 'Upstairs', data: [] as number[] };
|
||||||
|
const basementHumiditySeries = { name: 'Downstairs', data: [] as number[] };
|
||||||
|
|
||||||
|
const mainPressureSeries = { name: 'Upstairs', data: [] as number[] };
|
||||||
|
const basementPressureSeries = { name: 'Downstairs', data: [] as number[] };
|
||||||
|
|
||||||
|
const end = new Date();
|
||||||
|
const start = subHours(end, 24);
|
||||||
|
|
||||||
|
indoorStore.getReadingValueHistoryGrouped(start, end, 15).then((groupedReadingsList) => {
|
||||||
|
groupedReadingsList.forEach((groupedReadings) => {
|
||||||
|
if (groupedReadings.name === 'main') {
|
||||||
|
categories.push(new Date(groupedReadings.bucket).getTime());
|
||||||
|
|
||||||
|
mainTemperatureSeries.data.push(ConvertCToF(groupedReadings.averageTemperature));
|
||||||
|
mainHumiditySeries.data.push(groupedReadings.averageHumidity);
|
||||||
|
mainPressureSeries.data.push(ConvertMillibarToInchesOfMercury(groupedReadings.averagePressure));
|
||||||
|
} else if (groupedReadings.name === 'basement') {
|
||||||
|
basementTemperatureSeries.data.push(ConvertCToF(groupedReadings.averageTemperature));
|
||||||
|
basementHumiditySeries.data.push(groupedReadings.averageHumidity);
|
||||||
|
basementPressureSeries.data.push(ConvertMillibarToInchesOfMercury(groupedReadings.averagePressure));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ready.value = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container
|
||||||
|
fluid
|
||||||
|
class="container">
|
||||||
|
<v-row
|
||||||
|
dense
|
||||||
|
align="start">
|
||||||
|
<v-col
|
||||||
|
sm="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="ready"
|
||||||
|
type="line"
|
||||||
|
title="Temperature"
|
||||||
|
unit="°F"
|
||||||
|
group="indoor"
|
||||||
|
:categories="categories"
|
||||||
|
:series="[mainTemperatureSeries, basementTemperatureSeries]"></ValueChart>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
sm="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="ready"
|
||||||
|
type="line"
|
||||||
|
title="Humidity"
|
||||||
|
unit="%"
|
||||||
|
group="indoor"
|
||||||
|
:categories="categories"
|
||||||
|
:series="[mainHumiditySeries, basementHumiditySeries]"></ValueChart>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
sm="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="ready"
|
||||||
|
type="line"
|
||||||
|
title="Pressure"
|
||||||
|
unit='"'
|
||||||
|
group="indoor"
|
||||||
|
:y-axis-decimal-points="1"
|
||||||
|
:categories="categories"
|
||||||
|
:series="[mainPressureSeries, basementPressureSeries]"></ValueChart>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-container class="fill-height">
|
|
||||||
<v-responsive class="align-center text-center fill-height">
|
|
||||||
Inside
|
|
||||||
</v-responsive>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
//
|
|
||||||
</script>
|
|
||||||
160
WebDisplay/src/pages/outdoor.vue
Normal file
160
WebDisplay/src/pages/outdoor.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { subHours } from 'date-fns';
|
||||||
|
import { useWeatherStore } from '@/stores/weatherStore';
|
||||||
|
import { ConvertPascalToInchesOfMercury } from '@/pressureConverter';
|
||||||
|
import ValueChart from '../components/ValueChart.vue';
|
||||||
|
|
||||||
|
const weatherStore = useWeatherStore();
|
||||||
|
|
||||||
|
const readingsReady = ref(false);
|
||||||
|
const windReady = ref(false);
|
||||||
|
|
||||||
|
const readingsCategories: number[] = [];
|
||||||
|
const windCategories: number[] = [];
|
||||||
|
|
||||||
|
const temperatureSeries = { name: 'Average Temperature', data: [] as number[] };
|
||||||
|
const humiditySeries = { name: 'Average Humidity', data: [] as number[] };
|
||||||
|
const pressureSeries = { name: 'Average Pressure', data: [] as number[] };
|
||||||
|
const lightSeries = { name: 'Average Light', data: [] as number[] };
|
||||||
|
const rainSeries = { name: 'Total Rain', data: [] as number[] };
|
||||||
|
|
||||||
|
const windMinimumSeries = { name: 'Minimum', data: [] as number[] };
|
||||||
|
const windAverageSeries = { name: 'Average', data: [] as number[] };
|
||||||
|
const windMaximumSeries = { name: 'Maximum', data: [] as number[] };
|
||||||
|
|
||||||
|
const end = new Date();
|
||||||
|
const start = subHours(end, 24);
|
||||||
|
|
||||||
|
weatherStore.getReadingHistoryGrouped(start, end, 15).then((groupedReadingsList) => {
|
||||||
|
groupedReadingsList.forEach((groupedReadings) => {
|
||||||
|
readingsCategories.push(new Date(groupedReadings.bucket).getTime());
|
||||||
|
|
||||||
|
temperatureSeries.data.push(groupedReadings.averageTemperature);
|
||||||
|
humiditySeries.data.push(groupedReadings.averageHumidity);
|
||||||
|
pressureSeries.data.push(ConvertPascalToInchesOfMercury(groupedReadings.averagePressure));
|
||||||
|
lightSeries.data.push(groupedReadings.averageLightLevel);
|
||||||
|
rainSeries.data.push(groupedReadings.rainTotal);
|
||||||
|
});
|
||||||
|
|
||||||
|
readingsReady.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
weatherStore.getWindHistoryGrouped(start, end, 15).then((groupedReadingsList) => {
|
||||||
|
groupedReadingsList.forEach((groupedReadings) => {
|
||||||
|
windCategories.push(new Date(groupedReadings.bucket).getTime());
|
||||||
|
|
||||||
|
windMinimumSeries.data.push(groupedReadings.minimumSpeed);
|
||||||
|
windAverageSeries.data.push(groupedReadings.averageSpeed);
|
||||||
|
windMaximumSeries.data.push(groupedReadings.maximumSpeed);
|
||||||
|
});
|
||||||
|
|
||||||
|
windReady.value = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container
|
||||||
|
fluid
|
||||||
|
class="container">
|
||||||
|
<v-row
|
||||||
|
dense
|
||||||
|
align="start">
|
||||||
|
<v-col
|
||||||
|
sm="4"
|
||||||
|
xl="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="readingsReady"
|
||||||
|
type="line"
|
||||||
|
title="Temperature"
|
||||||
|
unit="°F"
|
||||||
|
group="outdoor"
|
||||||
|
:categories="readingsCategories"
|
||||||
|
:series="[temperatureSeries]">
|
||||||
|
</ValueChart>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
sm="4"
|
||||||
|
xl="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="readingsReady"
|
||||||
|
type="line"
|
||||||
|
title="Humidity"
|
||||||
|
unit="%"
|
||||||
|
group="outdoor"
|
||||||
|
:categories="readingsCategories"
|
||||||
|
:series="[humiditySeries]">
|
||||||
|
</ValueChart>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
sm="4"
|
||||||
|
xl="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="readingsReady"
|
||||||
|
type="line"
|
||||||
|
title="Pressure"
|
||||||
|
unit='"'
|
||||||
|
group="outdoor"
|
||||||
|
:y-axis-decimal-points="1"
|
||||||
|
:categories="readingsCategories"
|
||||||
|
:series="[pressureSeries]">
|
||||||
|
</ValueChart>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
sm="4"
|
||||||
|
xl="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="readingsReady"
|
||||||
|
type="line"
|
||||||
|
title="Light"
|
||||||
|
unit=" lx"
|
||||||
|
group="outdoor"
|
||||||
|
:categories="readingsCategories"
|
||||||
|
:series="[lightSeries]">
|
||||||
|
</ValueChart>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
sm="4"
|
||||||
|
xl="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="readingsReady"
|
||||||
|
type="line"
|
||||||
|
title="Rain"
|
||||||
|
unit='"'
|
||||||
|
group="outdoor"
|
||||||
|
:stepline="true"
|
||||||
|
:y-axis-decimal-points="2"
|
||||||
|
:categories="readingsCategories"
|
||||||
|
:series="[rainSeries]">
|
||||||
|
</ValueChart>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
sm="4"
|
||||||
|
xl="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="windReady"
|
||||||
|
type="line"
|
||||||
|
title="Wind"
|
||||||
|
unit=" MPH"
|
||||||
|
group="outdoor"
|
||||||
|
:y-axis-decimal-points="0"
|
||||||
|
:categories="windCategories"
|
||||||
|
:series="[windMinimumSeries, windAverageSeries, windMaximumSeries]">
|
||||||
|
</ValueChart>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-container class="fill-height">
|
|
||||||
<v-responsive class="align-center text-center fill-height">
|
|
||||||
Outside
|
|
||||||
</v-responsive>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
//
|
|
||||||
</script>
|
|
||||||
93
WebDisplay/src/pages/power.vue
Normal file
93
WebDisplay/src/pages/power.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { subHours } from 'date-fns';
|
||||||
|
import { usePowerStore } from '@/stores/powerStore';
|
||||||
|
import { useWeatherStore } from '@/stores/weatherStore';
|
||||||
|
import ValueChart from '../components/ValueChart.vue';
|
||||||
|
import WeatherValueType from '@/models/weather/weather-value-type';
|
||||||
|
|
||||||
|
const powerStore = usePowerStore();
|
||||||
|
const weatherStore = useWeatherStore();
|
||||||
|
|
||||||
|
const powerReady = ref(false);
|
||||||
|
const lightReady = ref(false);
|
||||||
|
|
||||||
|
const powerCategories: number[] = [];
|
||||||
|
const lightCategories: number[] = [];
|
||||||
|
|
||||||
|
const generationSeries = { name: 'Generation', data: [] as number[] };
|
||||||
|
const consumptionSeries = { name: 'Consumption', data: [] as number[] };
|
||||||
|
|
||||||
|
const lightSeries = { name: 'Average Light', data: [] as number[] };
|
||||||
|
|
||||||
|
const end = new Date();
|
||||||
|
const start = subHours(end, 24);
|
||||||
|
|
||||||
|
powerStore.getReadingHistoryGrouped(start, end, 15).then((groupedReadingsList) => {
|
||||||
|
groupedReadingsList.forEach((groupedReadings) => {
|
||||||
|
powerCategories.push(new Date(groupedReadings.bucket).getTime());
|
||||||
|
|
||||||
|
generationSeries.data.push(groupedReadings.averageGeneration);
|
||||||
|
consumptionSeries.data.push(groupedReadings.averageConsumption);
|
||||||
|
});
|
||||||
|
|
||||||
|
powerReady.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
weatherStore.getReadingValueHistoryGrouped(WeatherValueType.Light, start, end, 15).then((groupedReadingsList) => {
|
||||||
|
groupedReadingsList.forEach((groupedReadings) => {
|
||||||
|
lightCategories.push(new Date(groupedReadings.bucket).getTime());
|
||||||
|
|
||||||
|
lightSeries.data.push(groupedReadings.averageValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
lightReady.value = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container
|
||||||
|
fluid
|
||||||
|
class="container">
|
||||||
|
<v-row
|
||||||
|
dense
|
||||||
|
align="start">
|
||||||
|
<v-col
|
||||||
|
sm="4"
|
||||||
|
md="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="powerReady"
|
||||||
|
type="line"
|
||||||
|
title="Power"
|
||||||
|
unit=" W"
|
||||||
|
group="power"
|
||||||
|
:y-axis-decimal-points="0"
|
||||||
|
:value-decimal-points="0"
|
||||||
|
:categories="powerCategories"
|
||||||
|
:series="[generationSeries, consumptionSeries]"></ValueChart>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
sm="4"
|
||||||
|
md="6"
|
||||||
|
cols="12">
|
||||||
|
<ValueChart
|
||||||
|
:ready="lightReady"
|
||||||
|
type="line"
|
||||||
|
title="Light"
|
||||||
|
unit=" lx"
|
||||||
|
group="power"
|
||||||
|
:categories="lightCategories"
|
||||||
|
:series="[lightSeries]">
|
||||||
|
</ValueChart>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
WebDisplay/src/pressureConverter.ts
Normal file
7
WebDisplay/src/pressureConverter.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
62
WebDisplay/src/stores/indoorStore.ts
Normal file
62
WebDisplay/src/stores/indoorStore.ts
Normal file
@@ -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<ReadingsGrouped[]> {
|
||||||
|
const startString = start.toISOString();
|
||||||
|
const endString = end.toISOString();
|
||||||
|
|
||||||
|
const response = await axios.get<ReadingsGrouped[]>(
|
||||||
|
Environment.getUrlPrefix() +
|
||||||
|
`/api/environment/readings/history-grouped?start=${startString}&end=${endString}&bucketMinutes=${bucketMinutes}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
||||||
|
import axios from 'axios';
|
||||||
import Environment from '@/environment';
|
import Environment from '@/environment';
|
||||||
import PowerStatus from '@/models/power/power-status';
|
import PowerStatus from '@/models/power/power-status';
|
||||||
|
import PowerHistoryGrouped from '@/models/power/power-history-grouped';
|
||||||
|
|
||||||
export const usePowerStore = defineStore('power', {
|
export const usePowerStore = defineStore('power', {
|
||||||
state: () => {
|
state: () => {
|
||||||
@@ -36,6 +38,16 @@ export const usePowerStore = defineStore('power', {
|
|||||||
await this._connection.stop();
|
await this._connection.stop();
|
||||||
|
|
||||||
this._connection = null;
|
this._connection = null;
|
||||||
|
},
|
||||||
|
async getReadingHistoryGrouped(start: Date, end: Date, bucketMinutes: number): Promise<PowerHistoryGrouped[]> {
|
||||||
|
const startString = start.toISOString();
|
||||||
|
const endString = end.toISOString();
|
||||||
|
|
||||||
|
const response = await axios.get<PowerHistoryGrouped[]>(
|
||||||
|
Environment.getUrlPrefix() + `/api/power/status/history-grouped?start=${startString}&end=${endString}&bucketMinutes=${bucketMinutes}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import WeatherUpdate from '@/models/weather/weather-update';
|
|||||||
import WeatherRecent from '@/models/weather/weather-recent';
|
import WeatherRecent from '@/models/weather/weather-recent';
|
||||||
import WeatherValueType from '@/models/weather/weather-value-type';
|
import WeatherValueType from '@/models/weather/weather-value-type';
|
||||||
import WeatherValueGrouped from '@/models/weather/weather-value-grouped';
|
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', {
|
export const useWeatherStore = defineStore('weather', {
|
||||||
state: () => {
|
state: () => {
|
||||||
@@ -43,18 +45,11 @@ export const useWeatherStore = defineStore('weather', {
|
|||||||
this._connection = null;
|
this._connection = null;
|
||||||
},
|
},
|
||||||
async getLatest(): Promise<WeatherRecent> {
|
async getLatest(): Promise<WeatherRecent> {
|
||||||
const response = await axios.get<WeatherRecent>(
|
const response = await axios.get<WeatherRecent>(Environment.getUrlPrefix() + `/api/weather/readings/recent`);
|
||||||
Environment.getUrlPrefix() + `/api/weather/readings/recent`
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
async getReadingValueHistoryGrouped(
|
async getReadingValueHistoryGrouped(valueType: WeatherValueType, start: Date, end: Date, bucketMinutes: number): Promise<WeatherValueGrouped[]> {
|
||||||
valueType: WeatherValueType,
|
|
||||||
start: Date,
|
|
||||||
end: Date,
|
|
||||||
bucketMinutes: number
|
|
||||||
): Promise<WeatherValueGrouped[]> {
|
|
||||||
const startString = start.toISOString();
|
const startString = start.toISOString();
|
||||||
const endString = end.toISOString();
|
const endString = end.toISOString();
|
||||||
|
|
||||||
@@ -65,16 +60,32 @@ export const useWeatherStore = defineStore('weather', {
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
async getReadingAggregate(
|
async getReadingAggregate(start: Date, end: Date): Promise<WeatherAggregates | undefined> {
|
||||||
start: Date,
|
|
||||||
end: Date
|
|
||||||
): Promise<WeatherAggregates | undefined> {
|
|
||||||
const startString = start.toISOString();
|
const startString = start.toISOString();
|
||||||
const endString = end.toISOString();
|
const endString = end.toISOString();
|
||||||
|
|
||||||
const response = await axios.get<WeatherAggregates>(
|
const response = await axios.get<WeatherAggregates>(
|
||||||
Environment.getUrlPrefix() +
|
Environment.getUrlPrefix() + `/api/weather/readings/aggregate?start=${startString}&end=${endString}`
|
||||||
`/api/weather/readings/aggregate?start=${startString}&end=${endString}`
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
async getReadingHistoryGrouped(start: Date, end: Date, bucketMinutes: number): Promise<WeatherHistoryGrouped[]> {
|
||||||
|
const startString = start.toISOString();
|
||||||
|
const endString = end.toISOString();
|
||||||
|
|
||||||
|
const response = await axios.get<WeatherHistoryGrouped[]>(
|
||||||
|
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<WindHistoryGrouped[]> {
|
||||||
|
const startString = start.toISOString();
|
||||||
|
const endString = end.toISOString();
|
||||||
|
|
||||||
|
const response = await axios.get<WindHistoryGrouped[]>(
|
||||||
|
Environment.getUrlPrefix() + `/api/weather/readings/wind-history-grouped?start=${startString}&end=${endString}&bucketMinutes=${bucketMinutes}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
3
WebDisplay/src/temperatureConverter.ts
Normal file
3
WebDisplay/src/temperatureConverter.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function ConvertCToF(value: number): number {
|
||||||
|
return (value * 9.0) / 5.0 + 32.0;
|
||||||
|
}
|
||||||
5
WebDisplay/typed-router.d.ts
vendored
5
WebDisplay/typed-router.d.ts
vendored
@@ -40,8 +40,9 @@ import type {
|
|||||||
declare module 'vue-router/auto/routes' {
|
declare module 'vue-router/auto/routes' {
|
||||||
export interface RouteNamedMap {
|
export interface RouteNamedMap {
|
||||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||||
'/inside': RouteRecordInfo<'/inside', '/inside', Record<never, never>, Record<never, never>>,
|
'/indoor': RouteRecordInfo<'/indoor', '/indoor', Record<never, never>, Record<never, never>>,
|
||||||
'/outside': RouteRecordInfo<'/outside', '/outside', Record<never, never>, Record<never, never>>,
|
'/outdoor': RouteRecordInfo<'/outdoor', '/outdoor', Record<never, never>, Record<never, never>>,
|
||||||
|
'/power': RouteRecordInfo<'/power', '/power', Record<never, never>, Record<never, never>>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,6 @@ export default defineConfig({
|
|||||||
extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
|
extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 4200,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user