Switch new display to Vue

This commit is contained in:
2024-03-04 01:18:45 +00:00
parent a8e60c2e87
commit 4aaa9064fb
62 changed files with 2863 additions and 1569 deletions

View File

@@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

5
WebDisplay/.editorconfig Normal file
View File

@@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true

View File

@@ -1,18 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
};

20
WebDisplay/.eslintrc.js Normal file
View File

@@ -0,0 +1,20 @@
/**
* .eslint.js
*
* ESLint configuration file.
*/
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
],
rules: {
'vue/multi-word-component-names': 'off',
},
};

22
WebDisplay/.gitignore vendored
View File

@@ -1,22 +1,20 @@
# Logs
logs
*.log
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
.vscode
*.suo
*.ntvs*
*.njsproj

View File

@@ -1,10 +1,9 @@
{
"tabWidth": 4,
"useTabs": false,
"singleQuote": true,
"vueIndentScriptAndStyle": true,
"semi": true,
"singleAttributePerLine": true,
"bracketSpacing": true,
"printWidth": 150,
"bracketSameLine": true
"singleQuote": true,
"bracketSameLine": true,
"trailingComma": "none"
}

View File

@@ -1,30 +1,81 @@
# React + TypeScript + Vite
# Vuetify (Default)
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
Currently, two official plugins are available:
## ❗️ Important Links
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
- 📄 [Docs](https://vuetifyjs.com/)
- 🚨 [Issues](https://issues.vuetifyjs.com/)
- 🏬 [Store](https://store.vuetifyjs.com/)
- 🎮 [Playground](https://play.vuetifyjs.com/)
- 💬 [Discord](https://community.vuetifyjs.com)
## Expanding the ESLint configuration
## 💿 Install
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
- Configure the top-level `parserOptions` property like this:
| Package Manager | Command |
|---------------------------------------------------------------|----------------|
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
| [bun](https://bun.sh/#getting-started) | `bun install` |
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
After completing the installation, your environment is ready for Vuetify development.
## ✨ Features
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
- 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/)
-**Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
- 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc)
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
## 💡 Usage
This section covers how to start the development server and build your project for production.
### 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):
```bash
yarn dev
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
(Repeat for npm, pnpm, and bun with respective commands.)
> NODE_OPTIONS='--no-warnings' is added to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
### Building for Production
To build your project for production, use:
```bash
yarn build
```
(Repeat for npm, pnpm, and bun with respective commands.)
Once the build process is completed, your application will be ready for deployment in a production environment.
## 💪 Support Vuetify Development
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
## 📑 License
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2016-present Vuetify, LLC

19
WebDisplay/components.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
Almanac: typeof import('./src/components/Almanac.vue')['default']
CurrentLaundryStatus: typeof import('./src/components/CurrentLaundryStatus.vue')['default']
CurrentPower: typeof import('./src/components/CurrentPower.vue')['default']
CurrentWeather: typeof import('./src/components/CurrentWeather.vue')['default']
DashboardItem: typeof import('./src/components/DashboardItem.vue')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

View File

@@ -2,12 +2,13 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home Monitor</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,45 +1,46 @@
{
"name": "web-display",
"private": true,
"name": "WebDisplay",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"dev": "cross-env NODE_OPTIONS='--no-warnings' vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"lint": "eslint . --fix --ignore-path .gitignore"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mdi/font": "7.0.96",
"@microsoft/signalr": "^8.0.0",
"@types/react-grid-layout": "^1.3.5",
"@types/suncalc": "^1.9.2",
"axios": "^1.6.7",
"bootstrap": "^5.3.2",
"core-js": "^3.34.0",
"date-fns": "^3.3.1",
"localforage": "^1.10.0",
"match-sorter": "^6.3.4",
"react": "^18.2.0",
"react-bootstrap": "^2.10.0",
"react-dom": "^18.2.0",
"react-grid-layout": "^1.4.4",
"react-router-dom": "^6.22.0",
"sass": "^1.70.0",
"sort-by": "^1.2.0",
"suncalc": "^1.9.0"
"pinia": "^2.1.7",
"roboto-fontface": "*",
"suncalc": "^1.9.0",
"vue": "^3.3.0",
"vuetify": "^3.0.0"
},
"devDependencies": {
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"@babel/types": "^7.23.0",
"@types/node": "^20.10.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/eslint-config-typescript": "^12.0.0",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.1.0"
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-n": "^16.4.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.19.0",
"sass": "^1.69.0",
"typescript": "^5.3.0",
"unplugin-fonts": "^1.1.0",
"unplugin-vue-components": "^0.26.0",
"unplugin-vue-router": "^0.7.0",
"vite": "^5.0.0",
"vite-plugin-vuetify": "^2.0.0",
"vue-router": "^4.2.0",
"vue-tsc": "^1.8.0"
}
}

2359
WebDisplay/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
$primary: #663399;
@import 'bootstrap';
html {
height: 100%;
width: 100%;
}
body {
height: inherit;
width: inherit;
}
#root {
height: inherit;
width: inherit;
background-color: #fafafa;
}
.dropdown-toggle::after {
content: none !important;
}

View File

@@ -1,43 +0,0 @@
import './App.scss';
import { Routes, Route, Link } from 'react-router-dom';
import { Container, Nav, NavDropdown, Navbar } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBars } from '@fortawesome/free-solid-svg-icons';
import Dashboard from './views/dashboard/main';
function App() {
return (
<>
<Navbar variant="dark" bg="primary">
<Container fluid>
<Navbar.Brand as={Link} to="/">
Home Monitor
</Navbar.Brand>
<Nav>
<NavDropdown
title={
<FontAwesomeIcon
width={32}
height={32}
icon={faBars}
/>
}
align="end"
>
<NavDropdown.Item as={Link} to="/summary">
Summary
</NavDropdown.Item>
</NavDropdown>
</Nav>
</Container>
</Navbar>
<Routes>
<Route path="/" element={<Dashboard />} />
</Routes>
</>
);
}
export default App;

33
WebDisplay/src/App.vue Normal file
View File

@@ -0,0 +1,33 @@
<template>
<v-app>
<v-app-bar title="Home Monitor" color="primary"></v-app-bar>
<v-navigation-drawer expand-on-hover rail>
<v-list density="compact" nav>
<v-list-item
prepend-icon="mdi-view-dashboard"
title="Dashboard"
to="/">
</v-list-item>
<v-list-item
prepend-icon="mdi-sun-thermometer"
title="Outside"
to="outside">
</v-list-item>
<v-list-item
prepend-icon="mdi-home-analytics"
title="Inside"
to="inside">
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<script lang="ts" setup>
//
</script>

View File

@@ -0,0 +1,170 @@
<script lang="ts" setup>
import DashboardItem from './DashboardItem.vue';
import { useAlmanacStore } from '@/stores/almanacStore';
import { format, formatDuration, intervalToDuration } from 'date-fns';
const almanacStore = useAlmanacStore();
almanacStore.load();
const dayLength = (): string => {
const duration = intervalToDuration({
start: almanacStore.sunTimes!.sunrise,
end: almanacStore.sunTimes!.sunset
});
return formatDuration(duration, { format: ['hours', 'minutes'] });
};
const moonPhaseName = (): string => {
const phase = almanacStore.moonIllumination!.phase;
if (phase === 0) {
return 'New Moon';
} else if (phase < 0.25) {
return 'Waxing Crescent';
} else if (phase === 0.25) {
return 'First Quarter';
} else if (phase < 0.5) {
return 'Waxing Gibbous';
} else if (phase === 0.5) {
return 'Full Moon';
} else if (phase < 0.75) {
return 'Waning Gibbous';
} else if (phase === 0.75) {
return 'Last Quarter';
} else if (phase < 1.0) {
return 'Waning Crescent';
}
return '';
};
const moonPhaseLetter = (): string => {
const phase = almanacStore.moonIllumination!.phase;
if (phase === 0) {
return '0';
} else if (phase < 0.25) {
return 'D';
} else if (phase === 0.25) {
return 'G';
} else if (phase < 0.5) {
return 'I';
} else if (phase === 0.5) {
return '1';
} else if (phase < 0.75) {
return 'Q';
} else if (phase === 0.75) {
return 'T';
} else if (phase < 1.0) {
return 'W';
}
return '';
};
</script>
<template>
<DashboardItem title="Almanac">
<div className="almanac-content">
<div v-if="!almanacStore.sunTimes || !almanacStore.moonIllumination">Loading...</div>
<table v-else>
<tbody>
<tr>
<td className="almanac-table-header">Sunrise</td>
<td colSpan="{2}">
{{
format(
almanacStore.sunTimes.sunrise,
'hh:mm:ss aa'
)
}}
</td>
</tr>
<tr>
<td className="almanac-table-header">Sunset</td>
<td colSpan="{2}">
{{
format(
almanacStore.sunTimes.sunset,
'hh:mm:ss aa'
)
}}
</td>
</tr>
<tr>
<td className="almanac-table-header">Day length</td>
<td colSpan="{2}">{{ dayLength() }}</td>
</tr>
<tr v-if="almanacStore.moonTimes?.rise">
<td className="almanac-table-header">Moonrise</td>
<td colSpan="{2}">
{{
format(
almanacStore.moonTimes.rise,
'hh:mm:ss aa'
)
}}
</td>
</tr>
<tr v-if="almanacStore.moonTimes?.set">
<td className="almanac-table-header">Moonset</td>
<td colSpan="{2}">
{{
format(
almanacStore.moonTimes.set,
'hh:mm:ss aa'
)
}}
</td>
</tr>
<tr>
<td className="almanac-table-header">Moon</td>
<td>
{{ moonPhaseName() }}
<br />
{{
(
almanacStore.moonIllumination.fraction *
100
).toFixed(1)
}}% illuminated
</td>
<td>
<div className="moon-phase">
{{ moonPhaseLetter() }}
</div>
</td>
</tr>
</tbody>
</table>
</div>
</DashboardItem>
</template>
<style scoped>
@font-face {
font-family: moon;
src: url(/src/assets/moon_phases.ttf) format('opentype');
}
.almanac-content {
font-size: 14px;
padding: 6px 12px;
}
.almanac-table-header {
font-weight: 500;
text-align: right;
padding-right: 10px;
white-space: nowrap;
}
.moon-phase {
font-family: moon;
font-size: 28px;
margin-left: 10px;
display: block;
margin-top: 1px;
}
</style>

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import { useLaundryStore } from '@/stores/laundryStore';
const laundryStore = useLaundryStore();
laundryStore.start();
</script>
<template>
<DashboardItem title="Laundry">
<div className="laundry-current">
<div v-if="!laundryStore.current">Loading...</div>
<table v-else>
<tbody>
<tr v-if="laundryStore.current.dryer != null">
<td className="laundry-current-header">Dryer</td>
<td :className="laundryStore.current.dryer.toString()">
{{ laundryStore.current.dryer ? 'On' : 'Off' }}
</td>
</tr>
<tr v-if="laundryStore.current.washer != null">
<td className="laundry-current-header">Washer</td>
<td :className="laundryStore.current.washer.toString()">
{{ laundryStore.current.washer ? 'On' : 'Off' }}
</td>
</tr>
</tbody>
</table>
</div>
</DashboardItem>
</template>
<style>
.laundry-current {
font-size: 14px;
padding: 6px 12px;
}
.laundry-current-header {
font-weight: 500;
text-align: right;
padding-right: 10px;
}
.true {
color: darkgoldenrod;
}
.false {
color: darkgreen;
}
</style>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { usePowerStore } from '@/stores/powerStore';
const powerStore = usePowerStore();
powerStore.start();
</script>
<template>
<DashboardItem title="Power">
<div className="power-current">
<div v-if="!powerStore.current">Loading...</div>
<table v-else>
<tbody>
<tr>
<td className="power-current-header">Generation</td>
<td>
{{
powerStore.current!.Generation < 0
? 0
: powerStore.current!.Generation
}}
W
</td>
</tr>
<tr>
<td className="power-current-header">Consumption</td>
<td>
{{ powerStore.current!.Consumption }}
W
</td>
</tr>
</tbody>
</table>
</div>
</DashboardItem>
</template>
<style>
.power-current {
font-size: 14px;
padding: 6px 12px;
}
.power-current-header {
font-weight: 500;
text-align: right;
padding-right: 10px;
}
</style>

View File

@@ -0,0 +1,156 @@
<script setup lang="ts">
import { useWeatherStore } from '@/stores/weatherStore';
const weatherStore = useWeatherStore();
weatherStore.start();
const rotationClass = (pressureDifference: number | undefined) => {
if (!pressureDifference) {
return '';
} else if (Math.abs(pressureDifference) <= 1.0) {
return '';
} else if (pressureDifference > 1.0 && pressureDifference <= 2.0) {
return 'up-low';
} else if (pressureDifference > 2.0) {
return 'up-high';
} else if (pressureDifference < -1.0 && pressureDifference >= -2.0) {
return 'down-low';
} else if (pressureDifference < -2.0) {
return 'down-high';
}
return '';
};
</script>
<template>
<DashboardItem title="Weather">
<div className="weather-current">
<div v-if="!weatherStore.current">Loading...</div>
<table v-else>
<tbody>
<tr>
<td className="weather-current-header">Temperature</td>
<td>
{{
weatherStore.current?.Temperature?.toFixed(2)
}}°F
</td>
</tr>
<tr v-if="weatherStore.current?.HeatIndex">
<td className="weather-current-header">Heat index</td>
<td>
{{ weatherStore.current?.HeatIndex?.toFixed(2) }}°F
</td>
</tr>
<tr v-if="weatherStore.current?.WindChill">
<td className="weather-current-header">Wind chill</td>
<td>
{{ weatherStore.current?.WindChill?.toFixed(2) }}°F
</td>
</tr>
<tr>
<td className="weather-current-header">Humidity</td>
<td>
{{ weatherStore.current?.Humidity?.toFixed(2) }}%
</td>
</tr>
<tr>
<td className="weather-current-header">Dew point</td>
<td>
{{ weatherStore.current?.DewPoint?.toFixed(2) }}°F
</td>
</tr>
<tr>
<td className="weather-current-header">Pressure</td>
<td>
{{
weatherStore.current?.Pressure &&
(
weatherStore.current?.Pressure /
33.864 /
100
)?.toFixed(2)
}}"
<span
class="pressure-trend-arrow"
:class="
rotationClass(
weatherStore.current
?.PressureDifferenceThreeHour
)
"
:title="
'3 Hour Change: ' +
weatherStore.current?.PressureDifferenceThreeHour?.toFixed(
1
)
">
</span>
</td>
</tr>
<tr>
<td className="weather-current-header">Wind</td>
<td>
{{ weatherStore.current?.WindSpeed?.toFixed(2) }}
mph {{ weatherStore.current?.WindDirection }}
</td>
</tr>
<tr>
<td className="weather-current-header">Rain</td>
<td>
{{
weatherStore.current?.RainLastHour?.toFixed(2)
}}" (last hour)
</td>
</tr>
<tr>
<td className="weather-current-header">Light</td>
<td>
{{ weatherStore.current?.LightLevel?.toFixed(2) }}
lx
</td>
</tr>
</tbody>
</table>
</div>
</DashboardItem>
</template>
<style>
.weather-current {
font-size: 14px;
padding: 6px 12px;
}
.weather-current-header {
font-weight: 500;
text-align: right;
padding-right: 10px;
}
.pressure-trend-arrow {
display: inline-block;
position: relative;
left: 6px;
transform: scale(1.25);
}
.down-high {
transform: rotate(60deg) scale(1.25);
}
.down-low {
transform: rotate(25deg) scale(1.25);
}
.up-high {
transform: rotate(-60deg) scale(1.25);
}
.up-low {
transform: rotate(-25deg) scale(1.25);
}
</style>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
defineProps(['title']);
</script>
<template>
<div class="bg-primary dashboard-item-header">
{{ title }}
</div>
<div class="dashboard-item-content">
<slot></slot>
</div>
</template>
<style scoped>
.dashboard-item-header {
padding: 2px 10px;
}
.dashboard-item-content {
background-color: white;
border: 1px solid lightgray;
}
</style>

View File

@@ -1,24 +0,0 @@
@font-face {
font-family: moon;
src: url(/src/assets/moon_phases.ttf) format('opentype');
}
.almanac-content {
font-size: 14px;
padding: 10px;
}
.almanac-table-header {
font-weight: 500;
text-align: right;
padding-right: 10px;
white-space: nowrap;
}
.moon-phase {
font-family: moon;
font-size: 28px;
margin-left: 10px;
display: block;
margin-top: 1px;
}

View File

@@ -1,134 +0,0 @@
import './main.scss';
import { useEffect, useState } from 'react';
import * as SunCalc from 'suncalc';
import DashboardItem from '../dashboard-item/main';
import WeatherService from '../../services/weather/main';
import WeatherRecent from '../../services/weather/weather-recent';
import { format, formatDuration, intervalToDuration } from 'date-fns';
function Almanac() {
const [loaded, setLoaded] = useState<boolean>(false);
const [sunTimes, setSunTimes] = useState<SunCalc.GetTimesResult | null>(null);
const [moonTimes, setMoonTimes] = useState<SunCalc.GetMoonTimes | null>(null);
const [moonIllumination, setMoonIllumination] = useState<SunCalc.GetMoonIlluminationResult | null>(null);
const weatherService = new WeatherService();
const dayLength = (): string => {
const duration = intervalToDuration({
start: sunTimes!.sunrise,
end: sunTimes!.sunset,
});
return formatDuration(duration, { format: ['hours', 'minutes'] });
};
const moonPhaseName = (): string => {
const phase = moonIllumination!.phase;
if (phase === 0) {
return 'New Moon';
} else if (phase < 0.25) {
return 'Waxing Crescent';
} else if (phase === 0.25) {
return 'First Quarter';
} else if (phase < 0.5) {
return 'Waxing Gibbous';
} else if (phase === 0.5) {
return 'Full Moon';
} else if (phase < 0.75) {
return 'Waning Gibbous';
} else if (phase === 0.75) {
return 'Last Quarter';
} else if (phase < 1.0) {
return 'Waning Crescent';
}
return '';
};
const moonPhaseLetter = (): string => {
const phase = moonIllumination!.phase;
if (phase === 0) {
return '0';
} else if (phase < 0.25) {
return 'D';
} else if (phase === 0.25) {
return 'G';
} else if (phase < 0.5) {
return 'I';
} else if (phase === 0.5) {
return '1';
} else if (phase < 0.75) {
return 'Q';
} else if (phase === 0.75) {
return 'T';
} else if (phase < 1.0) {
return 'W';
}
return '';
};
useEffect(() => {
weatherService.getLatest().then((weatherRecent: WeatherRecent) => {
const date = new Date();
setSunTimes(SunCalc.getTimes(date, weatherRecent?.latitude!, weatherRecent?.longitude!));
setMoonTimes(SunCalc.getMoonTimes(date, weatherRecent?.latitude!, weatherRecent?.longitude!));
setMoonIllumination(SunCalc.getMoonIllumination(date));
setLoaded(true);
});
}, []);
return (
<DashboardItem title="Almanac">
<div className="weather-current">
{!loaded && <div>Loading...</div>}
{loaded && (
<table>
<tbody>
<tr>
<td className="almanac-table-header">Sunrise</td>
<td colSpan={2}>{format(sunTimes!.sunrise, 'hh:mm:ss aa')}</td>
</tr>
<tr>
<td className="almanac-table-header">Sunset</td>
<td colSpan={2}>{format(sunTimes!.sunset, 'hh:mm:ss aa')}</td>
</tr>
<tr>
<td className="almanac-table-header">Day length</td>
<td colSpan={2}>{dayLength()}</td>
</tr>
<tr>
<td className="almanac-table-header">Moonrise</td>
<td colSpan={2}>{format(moonTimes!.rise, 'hh:mm:ss aa')}</td>
</tr>
<tr>
<td className="almanac-table-header">Moonset</td>
<td colSpan={2}>{format(moonTimes!.set, 'hh:mm:ss aa')}</td>
</tr>
<tr>
<td className="almanac-table-header">Moon</td>
<td>
{moonPhaseName()}
<br />
{(moonIllumination!.fraction * 100).toFixed(1)}% illuminated
</td>
<td>
<div className="moon-phase">{moonPhaseLetter()}</div>
</td>
</tr>
</tbody>
</table>
)}
</div>
</DashboardItem>
);
}
export default Almanac;

View File

@@ -1,7 +0,0 @@
.dashboard-item-header {
padding: 8px;
}
.dashboard-item-content {
padding: 0px 4px;
}

View File

@@ -1,20 +0,0 @@
import './main.scss';
import { PropsWithChildren } from 'react';
type Props = {
title: string;
};
function DashboardItem(props: PropsWithChildren<Props>) {
return (
<>
<div className="dashboard-item-header bg-primary text-white">
{props.title}
</div>
<div className="dashboard-item-content">{props.children}</div>
</>
);
}
export default DashboardItem;

View File

@@ -1,18 +0,0 @@
.laundry-current {
font-size: 14px;
padding: 10px;
}
.laundry-current-header {
font-weight: 500;
text-align: right;
padding-right: 10px;
}
.true {
color: darkgoldenrod;
}
.false {
color: darkgreen;
}

View File

@@ -1,48 +0,0 @@
import './main.scss';
import { useEffect, useState } from 'react';
import DashboardItem from '../dashboard-item/main';
import LaundryService from '../../services/laundry/main';
import LaundryStatus from '../../services/laundry/laundry-status';
function Laundry() {
const [latestStatus, setLatestStatus] = useState<LaundryStatus | null>(null);
const laundryService = new LaundryService();
useEffect(() => {
laundryService.getLatest().then((status) => {
setLatestStatus(status);
});
laundryService.start((laundryStatus: LaundryStatus) => {
setLatestStatus(laundryStatus);
});
}, []);
return (
<DashboardItem title="Laundry">
<div className="laundry-current">
{latestStatus === null && <div>Loading...</div>}
{latestStatus !== null && (
<div>
<table>
<tbody>
<tr>
<td className="laundry-current-header">Washer</td>
<td className={latestStatus!.washer!.toString()}>{latestStatus!.washer ? 'On' : 'Off'}</td>
</tr>
<tr>
<td className="laundry-current-header">Dryer</td>
<td className={latestStatus!.dryer!.toString()}>{latestStatus!.dryer ? 'On' : 'Off'}</td>
</tr>
</tbody>
</table>
</div>
)}
</div>
</DashboardItem>
);
}
export default Laundry;

View File

@@ -1,10 +0,0 @@
.power-current {
font-size: 14px;
padding: 10px;
}
.power-current-header {
font-weight: 500;
text-align: right;
padding-right: 10px;
}

View File

@@ -1,44 +0,0 @@
import './main.scss';
import { useEffect, useState } from 'react';
import DashboardItem from '../dashboard-item/main';
import PowerService from '../../services/power/main';
import PowerStatus from '../../services/power/power-status';
function Power() {
const [latestStatus, setLatestStatus] = useState<PowerStatus | null>(null);
const powerService = new PowerService();
useEffect(() => {
powerService.start((powerStatus: PowerStatus) => {
setLatestStatus(powerStatus);
});
}, []);
return (
<DashboardItem title="Power">
<div className="power-current">
{latestStatus === null && <div>Loading...</div>}
{latestStatus !== null && (
<div>
<table>
<tbody>
<tr>
<td className="power-current-header">Generation</td>
<td>{latestStatus!.Generation < 0 ? 0 : latestStatus!.Generation} W</td>
</tr>
<tr>
<td className="power-current-header">Consumption</td>
<td>{latestStatus!.Consumption < 0 ? 0 : latestStatus!.Consumption} W</td>
</tr>
</tbody>
</table>
</div>
)}
</div>
</DashboardItem>
);
}
export default Power;

View File

@@ -1,33 +0,0 @@
.weather-current {
font-size: 14px;
padding: 10px;
}
.weather-current-header {
font-weight: 500;
text-align: right;
padding-right: 10px;
}
.pressure-trend-arrow {
display: inline-block;
position: relative;
left: 6px;
transform: scale(1.25);
}
.down-high {
transform: rotate(60deg) scale(1.25);
}
.down-low {
transform: rotate(25deg) scale(1.25);
}
.up-high {
transform: rotate(-60deg) scale(1.25);
}
.up-low {
transform: rotate(-25deg) scale(1.25);
}

View File

@@ -1,110 +0,0 @@
import './main.scss';
import { useEffect, useState } from 'react';
import DashboardItem from '../../dashboard-item/main';
import WeatherService from '../../../services/weather/main';
import WeatherUpdate from '../../../services/weather/weather-update';
function CurrentWeather() {
const [latestReading, setLatestReading] = useState<WeatherUpdate | null>(null);
const weatherService = new WeatherService();
useEffect(() => {
weatherService.start((weatherUpdate: WeatherUpdate) => {
setLatestReading(weatherUpdate);
});
}, []);
const rotationClass = (pressureDifference: number | undefined) => {
if (!pressureDifference) {
return '';
} else if (Math.abs(pressureDifference) <= 1.0) {
return '';
} else if (pressureDifference > 1.0 && pressureDifference <= 2.0) {
return 'up-low';
} else if (pressureDifference > 2.0) {
return 'up-high';
} else if (pressureDifference < -1.0 && pressureDifference >= -2.0) {
return 'down-low';
} else if (pressureDifference < -2.0) {
return 'down-high';
}
return '';
};
return (
<DashboardItem title="Weather">
<div className="weather-current">
{latestReading === null && <div>Loading...</div>}
{latestReading !== null && (
<table>
<tbody>
<tr>
<td className="weather-current-header">Temperature</td>
<td>
{latestReading!.Temperature?.toFixed(2)}
°F
</td>
</tr>
{latestReading!.HeatIndex && (
<tr>
<td className="weather-current-header">Heat index</td>
<td>
{latestReading!.HeatIndex?.toFixed(2)}
°F
</td>
</tr>
)}
{latestReading!.WindChill && (
<tr>
<td className="weather-current-header">Wind chill</td>
<td>{latestReading!.WindChill?.toFixed(2)}°F</td>
</tr>
)}
<tr>
<td className="weather-current-header">Humidity</td>
<td>{latestReading!.Humidity?.toFixed(2)}%</td>
</tr>
<tr>
<td className="weather-current-header">Dew point</td>
<td>
{latestReading!.DewPoint?.toFixed(2)}
°F
</td>
</tr>
<tr>
<td className="weather-current-header">Pressure</td>
<td>
{latestReading!.Pressure && (latestReading!.Pressure / 33.864 / 100)?.toFixed(2)}"
<span
className={'pressure-trend-arrow ' + rotationClass(latestReading!.PressureDifferenceThreeHour)}
title={'3 Hour Change: ' + latestReading!.PressureDifferenceThreeHour?.toFixed(1)}>
</span>
</td>
</tr>
<tr>
<td className="weather-current-header">Wind</td>
<td>
{latestReading!.WindSpeed?.toFixed(2)} mph {latestReading!.WindDirection}
</td>
</tr>
<tr>
<td className="weather-current-header">Rain</td>
<td>{latestReading!.RainLastHour?.toFixed(2)}" (last hour)</td>
</tr>
<tr>
<td className="weather-current-header">Light</td>
<td>{latestReading!.LightLevel?.toFixed(2)} lx</td>
</tr>
</tbody>
</table>
)}
</div>
</DashboardItem>
);
}
export default CurrentWeather;

View File

@@ -1,3 +1,3 @@
{
"API_PREFIX": ""
"API_PREFIX": "http://home.kaczorzoo.net"
}

14
WebDisplay/src/main.ts Normal file
View File

@@ -0,0 +1,14 @@
import { registerPlugins } from '@/plugins';
import App from './App.vue';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
const pinia = createPinia();
const app = createApp(App);
registerPlugins(app);
app.use(pinia);
app.mount('#app');

View File

@@ -1,13 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

View File

@@ -0,0 +1,4 @@
export default class WeatherValueGrouped {
bucket: string | undefined;
averageValue: number | undefined;
}

View File

@@ -0,0 +1,8 @@
export enum WeatherValueType {
Humidity = 'Humidity',
HumidityTemperature = 'HumidityTemperature',
PressureTemperature = 'PressureTemperature',
Pressure = 'Pressure'
}
export default WeatherValueType;

View File

@@ -0,0 +1,4 @@
export default class WeatherValue {
timestamp: string | undefined;
value: number | undefined;
}

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import CurrentWeather from '../components/CurrentWeather.vue';
import CurrentPower from '../components/CurrentPower.vue';
</script>
<template>
<v-container fluid class="container">
<v-row align="start">
<v-col cols="3">
<CurrentWeather></CurrentWeather>
</v-col>
<v-col cols="3">
<Almanac></Almanac>
</v-col>
<v-col cols="2">
<CurrentPower></CurrentPower>
</v-col>
<v-col cols="2">
<CurrentLaundryStatus></CurrentLaundryStatus>
</v-col>
</v-row>
</v-container>
</template>
<style scoped>
.container {
height: 100%;
background-color: #fafafa;
}
</style>

View File

@@ -0,0 +1,11 @@
<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>

View File

@@ -0,0 +1,11 @@
<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>

View File

@@ -0,0 +1,8 @@
import vuetify from './vuetify';
import router from '../router';
import type { App } from 'vue';
export function registerPlugins(app: App) {
app.use(vuetify).use(router);
}

View File

@@ -0,0 +1,20 @@
import '@mdi/font/css/materialdesignicons.css';
import 'vuetify/styles';
import { ThemeDefinition, createVuetify } from 'vuetify';
const theme: ThemeDefinition = {
dark: false,
colors: {
primary: '#602f6b'
}
};
export default createVuetify({
theme: {
defaultTheme: 'theme',
themes: {
theme
}
}
});

View File

@@ -0,0 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router/auto';
const router = createRouter({
history: createWebHistory(process.env.BASE_URL)
});
export default router;

View File

@@ -1,64 +0,0 @@
import { HubConnectionBuilder, HubConnection } from '@microsoft/signalr';
import axios from 'axios';
import DeviceMessage from './device-message';
import LaundryStatus from './laundry-status';
import Environment from '../../environment';
export default class LaundryService {
private connection: HubConnection;
private started: boolean = false;
private latestStatus: LaundryStatus = new LaundryStatus();
constructor() {
this.connection = new HubConnectionBuilder()
.withUrl(Environment.getUrlPrefix() + '/api/hub/device-status', {
withCredentials: false,
})
.build();
}
async getLatest(): Promise<LaundryStatus> {
const response = await axios.get<DeviceMessage[]>(Environment.getUrlPrefix() + `/api/device-status/status/recent`);
const newStatus = new LaundryStatus();
response.data.forEach((deviceMessage) => {
if (deviceMessage.name === 'washer') {
newStatus.washer = deviceMessage.status;
} else if (deviceMessage.name === 'dryer') {
newStatus.dryer = deviceMessage.status;
}
});
return newStatus;
}
async start(callback: (laundryStatus: LaundryStatus) => void) {
if (this.started) {
return;
}
this.started = true;
this.latestStatus = await this.getLatest();
await this.connection.start();
this.connection!.on('LatestStatus', (message: string) => {
const deviceMessage = JSON.parse(message) as DeviceMessage;
const newStatus = new LaundryStatus();
newStatus.dryer = this.latestStatus.dryer;
newStatus.washer = this.latestStatus.washer;
if (deviceMessage.name === 'washer') {
newStatus.washer = deviceMessage.status;
} else if (deviceMessage.name === 'dryer') {
newStatus.dryer = deviceMessage.status;
}
callback(newStatus);
});
}
}

View File

@@ -1,30 +0,0 @@
import { HubConnectionBuilder, HubConnection } from '@microsoft/signalr';
import PowerStatus from './power-status';
import Environment from '../../environment';
export default class PowerService {
private connection: HubConnection;
private started: boolean = false;
constructor() {
this.connection = new HubConnectionBuilder()
.withUrl(Environment.getUrlPrefix() + '/api/hub/power', {
withCredentials: false,
})
.build();
}
async start(callback: (powerStatus: PowerStatus) => void) {
if (this.started) {
return;
}
this.started = true;
await this.connection.start();
this.connection!.on('LatestSample', (message: string) => {
callback(JSON.parse(message));
});
}
}

View File

@@ -1,38 +0,0 @@
import { HubConnectionBuilder, HubConnection } from '@microsoft/signalr';
import WeatherUpdate from './weather-update';
import WeatherRecent from './weather-recent';
import Environment from '../../environment';
import axios from 'axios';
export default class WeatherService {
private connection: HubConnection;
private started: boolean = false;
constructor() {
this.connection = new HubConnectionBuilder()
.withUrl(Environment.getUrlPrefix() + '/api/hub/weather', {
withCredentials: false,
})
.build();
}
async getLatest(): Promise<WeatherRecent> {
const response = await axios.get<WeatherRecent>(Environment.getUrlPrefix() + `/api/weather/readings/recent`);
return response.data;
}
async start(callback: (weatherUpdate: WeatherUpdate) => void) {
if (this.started) {
return;
}
this.started = true;
await this.connection.start();
this.connection!.on('LatestReading', (message: string) => {
callback(JSON.parse(message));
});
}
}

View File

@@ -0,0 +1,37 @@
import { defineStore } from 'pinia';
import * as SunCalc from 'suncalc';
import WeatherRecent from '@/models/weather/weather-recent';
import { useWeatherStore } from './weatherStore';
export const useAlmanacStore = defineStore('almanac', {
state: () => {
return {
sunTimes: null as SunCalc.GetTimesResult | null,
moonTimes: null as SunCalc.GetMoonTimes | null,
moonIllumination: null as SunCalc.GetMoonIlluminationResult | null
};
},
actions: {
async load() {
const weatherStore = useWeatherStore();
weatherStore.getLatest().then((weatherRecent: WeatherRecent) => {
const date = new Date();
this.sunTimes = SunCalc.getTimes(
date,
weatherRecent?.latitude!,
weatherRecent?.longitude!
);
this.moonTimes = SunCalc.getMoonTimes(
date,
weatherRecent?.latitude!,
weatherRecent?.longitude!
);
this.moonIllumination = SunCalc.getMoonIllumination(date);
});
}
}
});

View File

@@ -0,0 +1,78 @@
import { defineStore } from 'pinia';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import axios from 'axios';
import Environment from '@/environment';
import LaundryStatus from '@/models/laundry/laundry-status';
import DeviceMessage from '@/models/laundry/device-message';
export const useLaundryStore = defineStore('laundry', {
state: () => {
return {
current: null as LaundryStatus | null,
_connection: null as HubConnection | null
};
},
actions: {
async start() {
if (this._connection) {
return;
}
this._connection = new HubConnectionBuilder()
.withUrl(
Environment.getUrlPrefix() + '/api/hub/device-status',
{
withCredentials: false
}
)
.build();
await this._connection.start();
this._connection.on('LatestStatus', (message: string) => {
const deviceMessage = JSON.parse(message) as DeviceMessage;
const newStatus = new LaundryStatus();
newStatus.dryer = this.current?.dryer;
newStatus.washer = this.current?.washer;
if (deviceMessage.name === 'washer') {
newStatus.washer = deviceMessage.status;
} else if (deviceMessage.name === 'dryer') {
newStatus.dryer = deviceMessage.status;
}
this.$patch({ current: newStatus });
});
this._connection.send('RequestLatestStatus');
},
async stop() {
if (!this._connection) {
return;
}
await this._connection.stop();
this._connection = null;
},
async getLatest(): Promise<LaundryStatus> {
const response = await axios.get<DeviceMessage[]>(
Environment.getUrlPrefix() + `/api/device-status/status/recent`
);
const newStatus = new LaundryStatus();
response.data.forEach((deviceMessage: DeviceMessage) => {
if (deviceMessage.name === 'washer') {
newStatus.washer = deviceMessage.status;
} else if (deviceMessage.name === 'dryer') {
newStatus.dryer = deviceMessage.status;
}
});
return newStatus;
}
}
});

View File

@@ -0,0 +1,41 @@
import { defineStore } from 'pinia';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import Environment from '@/environment';
import PowerStatus from '@/models/power/power-status';
export const usePowerStore = defineStore('power', {
state: () => {
return {
current: null as PowerStatus | null,
_connection: null as HubConnection | null
};
},
actions: {
async start() {
if (this._connection) {
return;
}
this._connection = new HubConnectionBuilder()
.withUrl(Environment.getUrlPrefix() + '/api/hub/power', {
withCredentials: false
})
.build();
await this._connection.start();
this._connection.on('LatestSample', (message: string) => {
this.$patch({ current: JSON.parse(message) });
});
},
async stop() {
if (!this._connection) {
return;
}
await this._connection.stop();
this._connection = null;
}
}
});

View File

@@ -0,0 +1,68 @@
import { defineStore } from 'pinia';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import axios from 'axios';
import Environment from '@/environment';
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';
export const useWeatherStore = defineStore('weather', {
state: () => {
return {
current: null as WeatherUpdate | null,
_connection: null as HubConnection | null
};
},
actions: {
async start() {
if (this._connection) {
return;
}
this._connection = new HubConnectionBuilder()
.withUrl(Environment.getUrlPrefix() + '/api/hub/weather', {
withCredentials: false
})
.build();
await this._connection.start();
this._connection.on('LatestReading', (message: string) => {
this.$patch({ current: JSON.parse(message) });
});
},
async stop() {
if (!this._connection) {
return;
}
await this._connection.stop();
this._connection = null;
},
async getLatest(): Promise<WeatherRecent> {
const response = await axios.get<WeatherRecent>(
Environment.getUrlPrefix() + `/api/weather/readings/recent`
);
return response.data;
},
async getReadingValueHistoryGrouped(
valueType: WeatherValueType,
start: Date,
end: Date,
bucketMinutes: number
): Promise<WeatherValueGrouped[]> {
const startString = start.toISOString();
const endString = end.toISOString();
const response = await axios.get<WeatherValueGrouped[]>(
Environment.getUrlPrefix() +
`/api/weather/readings/value-history-grouped?weatherValueType=${valueType}&start=${startString}&end=${endString}&bucketMinutes=${bucketMinutes}`
);
return response.data;
}
}
});

View File

@@ -0,0 +1,3 @@
html {
overflow: auto;
}

View File

@@ -1,6 +0,0 @@
@import '/node_modules/react-grid-layout/css/styles.css';
.dashboard-item {
background-color: white;
border: 1px solid lightgray;
}

View File

@@ -1,77 +0,0 @@
import './main.scss';
import { useState } from 'react';
import { Container, Form, Navbar } from 'react-bootstrap';
import RGL, { WidthProvider } from 'react-grid-layout';
import CurrentWeather from '../../components/weather/current/main';
import Almanac from '../../components/almanac/main';
import Laundry from '../../components/laundry/main';
import Power from '../../components/power/main';
const ReactGridLayout = WidthProvider(RGL);
function Dashboard() {
const [locked, setLocked] = useState(true);
const defaultLayout = [
{ i: 'current-weather', x: 0, y: 0, w: 6, h: 7 },
{ i: 'almanac', x: 6, y: 0, w: 5, h: 7 },
{ i: 'laundry', x: 0, y: 7, w: 5, h: 5 },
{ i: 'power', x: 5, y: 7, w: 5, h: 5 },
];
const storedLayout = localStorage.getItem('dashboard-layout');
const layout = storedLayout ? JSON.parse(storedLayout) : defaultLayout;
const onLayoutChange = (layout: RGL.Layout[]) => {
localStorage.setItem('dashboard-layout', JSON.stringify(layout));
};
return (
<Container fluid>
<Navbar>
<Form>
<Form.Check
id="dashboard-lock"
type="switch"
label="Locked"
defaultChecked={locked}
onChange={() => setLocked(!locked)}
/>
</Form>
</Navbar>
<ReactGridLayout
className="layout"
layout={layout}
cols={20}
rowHeight={30}
isDraggable={!locked}
isResizable={!locked}
onLayoutChange={onLayoutChange}>
<div
className="dashboard-item"
key="current-weather">
<CurrentWeather />
</div>
<div
className="dashboard-item"
key="almanac">
<Almanac />
</div>
<div
className="dashboard-item"
key="laundry">
<Laundry />
</div>
<div
className="dashboard-item"
key="power">
<Power />
</div>
</ReactGridLayout>
</Container>
);
}
export default Dashboard;

View File

@@ -1 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -1,25 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"baseUrl": ".",
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }],
"exclude": ["node_modules"]
}

View File

@@ -1,11 +1,9 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
"include": ["vite.config.mts"]
}

143
WebDisplay/typed-router.d.ts vendored Normal file
View File

@@ -0,0 +1,143 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
/// <reference types="unplugin-vue-router/client" />
import type {
// type safe route locations
RouteLocationTypedList,
RouteLocationResolvedTypedList,
RouteLocationNormalizedTypedList,
RouteLocationNormalizedLoadedTypedList,
RouteLocationAsString,
RouteLocationAsRelativeTypedList,
RouteLocationAsPathTypedList,
// helper types
// route definitions
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
// vue-router extensions
_RouterTyped,
RouterLinkTyped,
RouterLinkPropsTyped,
NavigationGuard,
UseLinkFnTyped,
// data fetching
_DataLoader,
_DefineLoaderOptions,
} from 'unplugin-vue-router/types'
declare module 'vue-router/auto/routes' {
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/inside': RouteRecordInfo<'/inside', '/inside', Record<never, never>, Record<never, never>>,
'/outside': RouteRecordInfo<'/outside', '/outside', Record<never, never>, Record<never, never>>,
}
}
declare module 'vue-router/auto' {
import type { RouteNamedMap } from 'vue-router/auto/routes'
export type RouterTyped = _RouterTyped<RouteNamedMap>
/**
* Type safe version of `RouteLocationNormalized` (the type of `to` and `from` in navigation guards).
* Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationNormalized<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocationNormalizedLoaded` (the return type of `useRoute()`).
* Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationNormalizedLoaded<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`).
* Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationResolved<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationResolvedTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocation<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocationRaw` . Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationRaw<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
| RouteLocationAsString<RouteNamedMap>
| RouteLocationAsRelativeTypedList<RouteNamedMap>[Name]
| RouteLocationAsPathTypedList<RouteNamedMap>[Name]
/**
* Generate a type safe params for a route location. Requires the name of the route to be passed as a generic.
*/
export type RouteParams<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['params']
/**
* Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic.
*/
export type RouteParamsRaw<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['paramsRaw']
export function useRouter(): RouterTyped
export function useRoute<Name extends keyof RouteNamedMap = keyof RouteNamedMap>(name?: Name): RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
export const useLink: UseLinkFnTyped<RouteNamedMap>
export function onBeforeRouteLeave(guard: NavigationGuard<RouteNamedMap>): void
export function onBeforeRouteUpdate(guard: NavigationGuard<RouteNamedMap>): void
export const RouterLink: RouterLinkTyped<RouteNamedMap>
export const RouterLinkProps: RouterLinkPropsTyped<RouteNamedMap>
// Experimental Data Fetching
export function defineLoader<
P extends Promise<any>,
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
isLazy extends boolean = false,
>(
name: Name,
loader: (route: RouteLocationNormalizedLoaded<Name>) => P,
options?: _DefineLoaderOptions<isLazy>,
): _DataLoader<Awaited<P>, isLazy>
export function defineLoader<
P extends Promise<any>,
isLazy extends boolean = false,
>(
loader: (route: RouteLocationNormalizedLoaded) => P,
options?: _DefineLoaderOptions<isLazy>,
): _DataLoader<Awaited<P>, isLazy>
export {
_definePage as definePage,
_HasDataLoaderMeta as HasDataLoaderMeta,
_setupDataFetchingGuard as setupDataFetchingGuard,
_stopDataFetchingScope as stopDataFetchingScope,
} from 'unplugin-vue-router/runtime'
}
declare module 'vue-router' {
import type { RouteNamedMap } from 'vue-router/auto/routes'
export interface TypesConfig {
beforeRouteUpdate: NavigationGuard<RouteNamedMap>
beforeRouteLeave: NavigationGuard<RouteNamedMap>
$route: RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[keyof RouteNamedMap]
$router: _RouterTyped<RouteNamedMap>
RouterLink: RouterLinkTyped<RouteNamedMap>
}
}

View File

@@ -0,0 +1,48 @@
// Plugins
import Components from 'unplugin-vue-components/vite';
import Vue from '@vitejs/plugin-vue';
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';
import ViteFonts from 'unplugin-fonts/vite';
import VueRouter from 'unplugin-vue-router/vite';
// Utilities
import { defineConfig } from 'vite';
import { fileURLToPath, URL } from 'node:url';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter(),
Vue({
template: { transformAssetUrls },
}),
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
Vuetify({
autoImport: true,
styles: {
configFile: 'src/styles/settings.scss',
},
}),
Components(),
ViteFonts({
google: {
families: [
{
name: 'Roboto',
styles: 'wght@100;300;400;500;700;900',
},
],
},
}),
],
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
},
server: {
port: 3000,
},
});

View File

@@ -1,7 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});