mirror of
https://github.com/ckaczor/HomeMonitor.git
synced 2026-06-15 09:45:07 -04:00
Compare commits
80 Commits
dependabot
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0384879bea | |||
| c088ab8292 | |||
| 8de424087f | |||
| ad70456a66 | |||
| 2f25286c21 | |||
| 98fa161eb1 | |||
| d8e760931e | |||
| e00bb35589 | |||
| 2f47d4c426 | |||
| 28c82dc2c3 | |||
| 601b6d8299 | |||
| d8096e92f0 | |||
| 4bf3fe204c | |||
| f007764d66 | |||
| fba31dfc33 | |||
| 2c30d131c5 | |||
| 6454bba7c8 | |||
| 766fa7c0cf | |||
| 8a15e06f35 | |||
| 89a428f8e7 | |||
| 86c39c252f | |||
| 5eb7d4a666 | |||
| d9a25a9832 | |||
| b27ccc1f49 | |||
| 6c00cf12e1 | |||
| 04a1bacb2b | |||
| 64f794b1b0 | |||
| d069167f59 | |||
| c2f112dfc9 | |||
| ba18ba2562 | |||
| 7431cd2233 | |||
| cb00c88197 | |||
| 29a58941ad | |||
| 79a8f837df | |||
| 2b52a15f96 | |||
| 215be2d5f0 | |||
| 28ed529cc0 | |||
| 5419f50d8b | |||
| 327bb6f7b9 | |||
| 8f8f4179f9 | |||
| edc4d13d85 | |||
| 532ee37169 | |||
| 2c740dd604 | |||
| 88cf5d1b83 | |||
| 28645d8bc3 | |||
| 200a07d6ba | |||
| 5e216867c1 | |||
| 87969ffe60 | |||
| da7c19714f | |||
| 26aadfb65f | |||
| 1ff4e1580e | |||
| 9ccc4ec9de | |||
| dc72f71e91 | |||
| 40ebbf38cb | |||
| 4dbedee94b | |||
| 031949409b | |||
| 5ea5019756 | |||
| 7edb527957 | |||
| 60563bc20b | |||
| bae01ed569 | |||
| e7eadf5167 | |||
| e13839dfc3 | |||
| dd67a7fa1c | |||
| ba1ab27fb0 | |||
| 36635224e8 | |||
| 32675c41bc | |||
| 419349baa4 | |||
| 7b24f82436 | |||
| d563e89c6b | |||
| 6925319487 | |||
| dc9320e12b | |||
| d24cb4555f | |||
| b4bd48a889 | |||
| 39625a7d82 | |||
| 94a818beda | |||
| 0e651143c1 | |||
| 3d7fa1afa6 | |||
| 4917493f21 | |||
| 6bf61966b8 | |||
| b1a9230f91 |
25
Calendar/Calendar.sln
Normal file
25
Calendar/Calendar.sln
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.8.34330.188
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "Service\Service.csproj", "{AAF49637-5EAB-4D0F-A3EB-4421456DF709}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{AAF49637-5EAB-4D0F-A3EB-4421456DF709}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AAF49637-5EAB-4D0F-A3EB-4421456DF709}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AAF49637-5EAB-4D0F-A3EB-4421456DF709}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AAF49637-5EAB-4D0F-A3EB-4421456DF709}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {AF8B4FE8-1A8C-4189-8A16-BFD216D13047}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
8
Calendar/Calendar.sln.DotSettings
Normal file
8
Calendar/Calendar.sln.DotSettings
Normal file
@@ -0,0 +1,8 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSOR_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Kaczor/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -1 +0,0 @@
|
||||
PORT=8080
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"windows"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
486
Calendar/Service/.gitignore
vendored
486
Calendar/Service/.gitignore
vendored
@@ -1,2 +1,484 @@
|
||||
dist/
|
||||
node_modules/
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from `dotnet new gitignore`
|
||||
|
||||
# dotenv files
|
||||
.env
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Tye
|
||||
.tye/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
.idea/
|
||||
|
||||
##
|
||||
## Visual studio for Mac
|
||||
##
|
||||
|
||||
|
||||
# globs
|
||||
Makefile.in
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
config.make
|
||||
config.status
|
||||
aclocal.m4
|
||||
install-sh
|
||||
autom4te.cache/
|
||||
*.tar.gz
|
||||
tarballs/
|
||||
test-results/
|
||||
|
||||
# Mac bundle stuff
|
||||
*.dmg
|
||||
*.app
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
51
Calendar/Service/Calendar.http
Normal file
51
Calendar/Service/Calendar.http
Normal file
@@ -0,0 +1,51 @@
|
||||
@Calendar_HostAddress = http://localhost:5060
|
||||
|
||||
GET {{Calendar_HostAddress}}/calendar/upcoming
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Calendar_HostAddress}}/calendar/upcoming?days=7
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Calendar_HostAddress}}/calendar/upcoming?includeHolidays=true
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Calendar_HostAddress}}/calendar/upcoming?days=7&includeHolidays=true
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Calendar_HostAddress}}/events/next
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Calendar_HostAddress}}/events/next?timezone=America/New_York
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Calendar_HostAddress}}/national-days/today
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Calendar_HostAddress}}/national-days/today?timezone=America/New_York
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Calendar_HostAddress}}/national-days/today?timezone=America/New_York&provider=DaysOfTheYear
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Calendar_HostAddress}}/national-days/today?timezone=America/New_York&provider=HolidayCalendar
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
40
Calendar/Service/Controllers/CalendarController.cs
Normal file
40
Calendar/Service/Controllers/CalendarController.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using ChrisKaczor.HomeMonitor.Calendar.Service.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Controllers;
|
||||
|
||||
[Route("calendar")]
|
||||
[ApiController]
|
||||
public class CalendarController(IConfiguration configuration, HttpClient httpClient) : ControllerBase
|
||||
{
|
||||
private async Task<IEnumerable<CalendarEntry>> GetCalendarEntries(string calendarUrl, int days, bool isHoliday)
|
||||
{
|
||||
var data = await httpClient.GetStringAsync(calendarUrl);
|
||||
|
||||
var calendar = Ical.Net.Calendar.Load(data);
|
||||
|
||||
var start = DateTimeOffset.Now.Date;
|
||||
var end = start.AddDays(days);
|
||||
|
||||
var calendarEntries = calendar
|
||||
.GetOccurrences(start, end)
|
||||
.Select(o => new CalendarEntry(o, isHoliday))
|
||||
.OrderBy(ce => ce.Start);
|
||||
|
||||
return calendarEntries;
|
||||
}
|
||||
|
||||
[HttpGet("upcoming")]
|
||||
public async Task<ActionResult<IEnumerable<CalendarEntry>>> GetUpcoming([FromQuery] int days = 1, [FromQuery] bool includeHolidays = false)
|
||||
{
|
||||
var calendarEntries = await GetCalendarEntries(configuration["Calendar:PersonalUrl"]!, days, false);
|
||||
|
||||
if (!includeHolidays)
|
||||
return Ok(calendarEntries);
|
||||
|
||||
var holidayEntries = await GetCalendarEntries(configuration["Calendar:HolidayUrl"]!, days, true);
|
||||
calendarEntries = calendarEntries.Concat(holidayEntries).OrderBy(c => c.Start);
|
||||
|
||||
return Ok(calendarEntries);
|
||||
}
|
||||
}
|
||||
35
Calendar/Service/Controllers/HolidayController.cs
Normal file
35
Calendar/Service/Controllers/HolidayController.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using ChrisKaczor.HomeMonitor.Calendar.Service.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Controllers;
|
||||
|
||||
[Route("events")]
|
||||
[ApiController]
|
||||
public class HolidayController(IConfiguration configuration, HttpClient httpClient) : ControllerBase
|
||||
{
|
||||
[HttpGet("next")]
|
||||
public async Task<ActionResult<IEnumerable<CalendarEntry>>> GetNext([FromQuery] string timezone = "Etc/UTC")
|
||||
{
|
||||
var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timezone);
|
||||
|
||||
var data = await httpClient.GetStringAsync(configuration["Calendar:HolidayUrl"]);
|
||||
|
||||
var calendar = Ical.Net.Calendar.Load(data);
|
||||
|
||||
var start = DateTimeOffset.Now.Date;
|
||||
var end = start.AddYears(1);
|
||||
|
||||
var calendarEntries = calendar
|
||||
.GetOccurrences(start, end)
|
||||
.Select(o => new CalendarEntry(o, true))
|
||||
.OrderBy(ce => ce.Start);
|
||||
|
||||
var nextCalendarEntry = calendarEntries.First();
|
||||
|
||||
var holidayEntry = new HolidayEntry(nextCalendarEntry, timeZoneInfo);
|
||||
|
||||
var holidayResponse = new HolidayResponse(holidayEntry, timeZoneInfo);
|
||||
|
||||
return Ok(holidayResponse);
|
||||
}
|
||||
}
|
||||
69
Calendar/Service/Controllers/NationalDaysController.cs
Normal file
69
Calendar/Service/Controllers/NationalDaysController.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using ChrisKaczor.HomeMonitor.Calendar.Service.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RestSharp;
|
||||
using DaysOfTheYear = ChrisKaczor.HomeMonitor.Calendar.Service.Models.DaysOfTheYear;
|
||||
using HolidayCalendar = ChrisKaczor.HomeMonitor.Calendar.Service.Models.HolidayCalendar;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Controllers;
|
||||
|
||||
[Route("national-days")]
|
||||
[ApiController]
|
||||
public class NationalDaysController(IConfiguration configuration, RestClient restClient) : ControllerBase
|
||||
{
|
||||
[HttpGet("today")]
|
||||
public async Task<ActionResult<IEnumerable<NationalDay>>> GetToday([FromQuery] string timezone = "Etc/UTC", [FromQuery] string provider = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(provider))
|
||||
{
|
||||
provider = configuration["Calendar:NationalDays:Provider"] ?? string.Empty;
|
||||
}
|
||||
|
||||
var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timezone);
|
||||
var timeZoneOffset = timeZoneInfo.GetUtcOffset(DateTimeOffset.Now);
|
||||
|
||||
if (provider == "DaysOfTheYear")
|
||||
{
|
||||
return Ok(await GetFromDaysOfTheYear(timeZoneOffset));
|
||||
}
|
||||
|
||||
return Ok(await GetFromHolidayCalendar(timeZoneOffset));
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<NationalDay>?> GetFromDaysOfTheYear(TimeSpan timeZoneOffset)
|
||||
{
|
||||
var timeZoneOffsetHours = timeZoneOffset.TotalHours;
|
||||
|
||||
var restRequest = new RestRequest(configuration["Calendar:DaysOfTheYear:Url"]);
|
||||
restRequest.AddHeader("X-Api-Key", configuration["Calendar:DaysOfTheYear:Key"] ?? string.Empty);
|
||||
restRequest.AddQueryParameter("timezone_offset", timeZoneOffsetHours);
|
||||
|
||||
var response = await restClient.GetAsync<DaysOfTheYear.Response>(restRequest);
|
||||
|
||||
var items = response?.Data.Where(d => d.Type == "day");
|
||||
|
||||
var nationalDays = items?.Select(i => new NationalDay(i));
|
||||
|
||||
return nationalDays;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<NationalDay>?> GetFromHolidayCalendar(TimeSpan timeZoneOffset)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow.ToOffset(timeZoneOffset);
|
||||
var dateString = now.ToString("yyyy-MM-dd");
|
||||
|
||||
var countries = configuration.GetSection("Calendar:HolidayCalendar:CountryCodes").Get<List<string>>() ?? [];
|
||||
|
||||
var restRequest = new RestRequest(configuration["Calendar:HolidayCalendar:Url"]);
|
||||
restRequest.AddHeader("Authorization", $"Bearer {configuration["Calendar:HolidayCalendar:Key"] ?? string.Empty}");
|
||||
restRequest.AddQueryParameter("limit", 100);
|
||||
restRequest.AddQueryParameter("type", "Day");
|
||||
restRequest.AddQueryParameter("startDate", dateString);
|
||||
restRequest.AddQueryParameter("endDate", dateString);
|
||||
|
||||
var response = await restClient.GetAsync<HolidayCalendar.Response>(restRequest);
|
||||
|
||||
var nationalDays = response?.Data.Items.Where(i => countries.Contains(i.Country.Code)).Select(i => new NationalDay(i));
|
||||
|
||||
return nationalDays;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
RUN npm run build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY ["./Service.csproj", "./"]
|
||||
RUN dotnet restore "Service.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src"
|
||||
RUN dotnet publish "Service.csproj" -c Release -o /app
|
||||
|
||||
CMD ["npm", "run", "dist"]
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app .
|
||||
RUN apk add --no-cache tzdata icu-libs
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||
ENTRYPOINT ["dotnet", "ChrisKaczor.HomeMonitor.Calendar.Service.dll"]
|
||||
15
Calendar/Service/Models/CalendarEntry.cs
Normal file
15
Calendar/Service/Models/CalendarEntry.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Ical.Net.CalendarComponents;
|
||||
using Ical.Net.DataTypes;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models;
|
||||
|
||||
[PublicAPI]
|
||||
public class CalendarEntry(Occurrence occurrence, bool isHoliday)
|
||||
{
|
||||
public string Summary { get; set; } = ((CalendarEvent)occurrence.Source).Summary;
|
||||
public bool IsAllDay { get; set; } = ((CalendarEvent)occurrence.Source).IsAllDay;
|
||||
public DateTimeOffset Start { get; set; } = occurrence.Period.StartTime.AsDateTimeOffset;
|
||||
public DateTimeOffset End { get; set; } = occurrence.Period.EndTime.AsDateTimeOffset;
|
||||
public bool IsHoliday { get; set; } = isHoliday;
|
||||
}
|
||||
17
Calendar/Service/Models/DaysOfTheYear/Entry.cs
Normal file
17
Calendar/Service/Models/DaysOfTheYear/Entry.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using JetBrains.Annotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models.DaysOfTheYear;
|
||||
|
||||
[PublicAPI]
|
||||
public class Entry
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
[JsonConverter(typeof(StringOrBooleanConverter))]
|
||||
public string Excerpt { get; set; } = string.Empty;
|
||||
|
||||
public string Type { get; set; } = string.Empty;
|
||||
}
|
||||
17
Calendar/Service/Models/DaysOfTheYear/Meta.cs
Normal file
17
Calendar/Service/Models/DaysOfTheYear/Meta.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using JetBrains.Annotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models.DaysOfTheYear;
|
||||
|
||||
[PublicAPI]
|
||||
public class Meta
|
||||
{
|
||||
[JsonPropertyName("cache_status")]
|
||||
public string CacheStatus { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("response_count")]
|
||||
public int ResponseCount { get; set; }
|
||||
|
||||
[JsonPropertyName("request_type")]
|
||||
public string RequestType { get; set; } = string.Empty;
|
||||
}
|
||||
11
Calendar/Service/Models/DaysOfTheYear/Response.cs
Normal file
11
Calendar/Service/Models/DaysOfTheYear/Response.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models.DaysOfTheYear;
|
||||
|
||||
[PublicAPI]
|
||||
public class Response
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public Meta Meta { get; set; } = new();
|
||||
public IEnumerable<Entry> Data { get; set; } = [];
|
||||
}
|
||||
19
Calendar/Service/Models/Duration.cs
Normal file
19
Calendar/Service/Models/Duration.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models;
|
||||
|
||||
public class Duration
|
||||
{
|
||||
public int Days { get; set; }
|
||||
public int Hours { get; set; }
|
||||
public int Minutes { get; set; }
|
||||
public int Seconds { get; set; }
|
||||
|
||||
public Duration(DateTimeOffset date)
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
var duration = date - now;
|
||||
Days = duration.Days;
|
||||
Hours = duration.Hours;
|
||||
Minutes = duration.Minutes;
|
||||
Seconds = duration.Seconds;
|
||||
}
|
||||
}
|
||||
11
Calendar/Service/Models/HolidayCalendar/Country.cs
Normal file
11
Calendar/Service/Models/HolidayCalendar/Country.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models.HolidayCalendar;
|
||||
|
||||
[PublicAPI]
|
||||
public class Country
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Code { get; set; }
|
||||
}
|
||||
13
Calendar/Service/Models/HolidayCalendar/Data.cs
Normal file
13
Calendar/Service/Models/HolidayCalendar/Data.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models.HolidayCalendar;
|
||||
|
||||
[PublicAPI]
|
||||
public class Data
|
||||
{
|
||||
public IEnumerable<Item> Items { get; set; } = [];
|
||||
public int Total { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int Limit { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
}
|
||||
14
Calendar/Service/Models/HolidayCalendar/Item.cs
Normal file
14
Calendar/Service/Models/HolidayCalendar/Item.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models.HolidayCalendar;
|
||||
|
||||
[PublicAPI]
|
||||
public class Item
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Excerpt { get; set; }
|
||||
public required string Url { get; set; }
|
||||
public required string Type { get; set; }
|
||||
public required Country Country { get; set; }
|
||||
}
|
||||
10
Calendar/Service/Models/HolidayCalendar/Response.cs
Normal file
10
Calendar/Service/Models/HolidayCalendar/Response.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models.HolidayCalendar;
|
||||
|
||||
[PublicAPI]
|
||||
public class Response
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public Data Data { get; set; } = new();
|
||||
}
|
||||
19
Calendar/Service/Models/HolidayEntry.cs
Normal file
19
Calendar/Service/Models/HolidayEntry.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models;
|
||||
|
||||
public class HolidayEntry
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public DateTimeOffset Date { get; set; }
|
||||
public string Type { get; set; }
|
||||
public bool IsToday { get; set; }
|
||||
public Duration DurationUntil { get; set; }
|
||||
|
||||
public HolidayEntry(CalendarEntry calendarEntry, TimeZoneInfo timeZoneInfo)
|
||||
{
|
||||
Name = calendarEntry.Summary;
|
||||
Date = TimeZoneInfo.ConvertTime(calendarEntry.Start, timeZoneInfo).Subtract(timeZoneInfo.GetUtcOffset(calendarEntry.Start));
|
||||
Type = "public";
|
||||
IsToday = Date.Date == DateTimeOffset.UtcNow.Date;
|
||||
DurationUntil = new Duration(Date);
|
||||
}
|
||||
}
|
||||
10
Calendar/Service/Models/HolidayResponse.cs
Normal file
10
Calendar/Service/Models/HolidayResponse.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models;
|
||||
|
||||
[PublicAPI]
|
||||
public class HolidayResponse(HolidayEntry holidayEntry, TimeZoneInfo timeZoneInfo)
|
||||
{
|
||||
public DateTimeOffset ResponseTime { get; set; } = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, timeZoneInfo);
|
||||
public HolidayEntry? Event { get; set; } = holidayEntry;
|
||||
}
|
||||
31
Calendar/Service/Models/NationalDay.cs
Normal file
31
Calendar/Service/Models/NationalDay.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using JetBrains.Annotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service.Models;
|
||||
|
||||
[PublicAPI]
|
||||
public class NationalDay
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string Url { get; set; }
|
||||
public required string Excerpt { get; set; }
|
||||
public required string Type { get; set; }
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public NationalDay(HolidayCalendar.Item item)
|
||||
{
|
||||
Name = item.Name;
|
||||
Url = item.Url;
|
||||
Excerpt = item.Excerpt;
|
||||
Type = item.Type;
|
||||
}
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public NationalDay(DaysOfTheYear.Entry entry)
|
||||
{
|
||||
Name = entry.Name;
|
||||
Url = entry.Url;
|
||||
Excerpt = entry.Excerpt;
|
||||
Type = entry.Type;
|
||||
}
|
||||
}
|
||||
41
Calendar/Service/Program.cs
Normal file
41
Calendar/Service/Program.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using ChrisKaczor.Common.OpenTelemetry;
|
||||
using RestSharp;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", corsPolicyBuilder => corsPolicyBuilder
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials()
|
||||
.WithOrigins("http://localhost:4200", "http://172.23.10.3:9001")));
|
||||
|
||||
builder.Configuration.AddEnvironmentVariables();
|
||||
|
||||
builder.Services.AddCommonOpenTelemetry(Assembly.GetExecutingAssembly().GetName().Name,
|
||||
builder.Configuration["Telemetry:Endpoint"]);
|
||||
|
||||
builder.Services.AddSingleton<HttpClient>();
|
||||
builder.Services.AddSingleton(new RestClient());
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// -- --
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseCors("CorsPolicy");
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
11
Calendar/Service/Properties/launchSettings.json
Normal file
11
Calendar/Service/Properties/launchSettings.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Calendar": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:5060"
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Calendar/Service/Service.csproj
Normal file
28
Calendar/Service/Service.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InvariantGlobalization>false</InvariantGlobalization>
|
||||
<RootNamespace>ChrisKaczor.HomeMonitor.Calendar.Service</RootNamespace>
|
||||
<AssemblyName>ChrisKaczor.HomeMonitor.Calendar.Service</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ChrisKaczor.Common.OpenTelemetry" Version="1.0.2" />
|
||||
<PackageReference Include="Ical.Net" Version="4.3.1" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0">
|
||||
<TreatAsUsed>true</TreatAsUsed>
|
||||
</PackageReference>
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
|
||||
<PackageReference Include="RestSharp" Version="112.1.0" />
|
||||
<PackageReference Include="System.Formats.Asn1" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
30
Calendar/Service/StringOrBooleanConverter.cs
Normal file
30
Calendar/Service/StringOrBooleanConverter.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Calendar.Service;
|
||||
|
||||
public class StringOrBooleanConverter : JsonConverter<string>
|
||||
{
|
||||
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.True => string.Empty,
|
||||
JsonTokenType.False => string.Empty,
|
||||
JsonTokenType.String => reader.GetString() ?? string.Empty,
|
||||
_ => throw new JsonException($"Unexpected token type: {reader.TokenType}")
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
|
||||
{
|
||||
if (bool.TryParse(value, out var boolValue))
|
||||
{
|
||||
writer.WriteBooleanValue(boolValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Calendar/Service/appsettings.Development.json
Normal file
16
Calendar/Service/appsettings.Development.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Calendar": {
|
||||
"PersonalUrl": "",
|
||||
"HolidayUrl": "",
|
||||
"NationalDays": {
|
||||
"Url": "",
|
||||
"Key": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Calendar/Service/appsettings.json
Normal file
28
Calendar/Service/appsettings.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Telemetry": {
|
||||
"Endpoint": "http://signoz-otel-collector.platform:4317/"
|
||||
},
|
||||
"Calendar": {
|
||||
"PersonalUrl": "",
|
||||
"HolidayUrl": "",
|
||||
"DaysOfTheYear": {
|
||||
"Url": "",
|
||||
"Key": ""
|
||||
},
|
||||
"HolidayCalendar": {
|
||||
"Url": "",
|
||||
"Key": "",
|
||||
"CountryCodes": [ "US", "INTL" ]
|
||||
},
|
||||
"NationalDays": {
|
||||
"Provider": "HolidayCalendar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,37 @@ spec:
|
||||
imagePullPolicy: Always
|
||||
securityContext:
|
||||
privileged: true
|
||||
env:
|
||||
- name: Calendar__PersonalUrl
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: calendar-config
|
||||
key: PERSONAL_URL
|
||||
- name: Calendar__HolidayUrl
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: calendar-config
|
||||
key: HOLIDAYS_URL
|
||||
- name: Calendar__DaysOfTheYear__Url
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: calendar-config
|
||||
key: DAYS_OF_THE_YEAR_URL
|
||||
- name: Calendar__DaysOfTheYear__Key
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: calendar-config
|
||||
key: DAYS_OF_THE_YEAR_KEY
|
||||
- name: Calendar__HolidayCalendar__Url
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: calendar-config
|
||||
key: HOLIDAY_CALENDAR_URL
|
||||
- name: Calendar__HolidayCalendar__Key
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: calendar-config
|
||||
key: HOLIDAY_CALENDAR_KEY
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
dnsPolicy: ClusterFirst
|
||||
|
||||
3166
Calendar/Service/package-lock.json
generated
3166
Calendar/Service/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "calendar",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "ts-node-dev --pretty --respawn ./src/app.ts",
|
||||
"build": "tsc -p ./",
|
||||
"dist": "node ./dist/app.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"date-holidays": "^3.23.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"helmet": "^7.1.0",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"luxon": "^3.4.4",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/luxon": "^3.3.8",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"eslint": "^8.56.0",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import { eventsRouter } from './events/events.routes';
|
||||
|
||||
if (!process.env.PORT) {
|
||||
console.log('No port value specified');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.env.TZ = 'Etc/UTC';
|
||||
|
||||
const PORT = parseInt(process.env.PORT as string, 10);
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cors());
|
||||
app.use(helmet());
|
||||
|
||||
app.use('/events/', eventsRouter);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is listening on port ${PORT}`);
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import { HolidaysTypes } from 'date-holidays';
|
||||
import { DateTime, Interval } from 'luxon';
|
||||
|
||||
export class Event {
|
||||
name: string;
|
||||
date: DateTime;
|
||||
type: HolidaysTypes.HolidayType;
|
||||
isToday: boolean;
|
||||
durationUntil: {
|
||||
days: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
} | null;
|
||||
|
||||
constructor(holiday: HolidaysTypes.Holiday, timezone: string) {
|
||||
const now = DateTime.now().setZone(timezone);
|
||||
this.name = holiday.name;
|
||||
this.date = DateTime.fromFormat(holiday.date, 'yyyy-MM-dd HH:mm:ss', {
|
||||
zone: timezone,
|
||||
}).startOf('day');
|
||||
this.type = holiday.type;
|
||||
this.isToday = this.date.hasSame(now, 'day');
|
||||
|
||||
const duration = Interval.fromDateTimes(now, this.date).toDuration([
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
'seconds',
|
||||
]);
|
||||
|
||||
this.durationUntil = !duration.isValid
|
||||
? null
|
||||
: {
|
||||
days: duration.days,
|
||||
hours: duration.hours,
|
||||
minutes: duration.minutes,
|
||||
seconds: Math.round(duration.seconds),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import * as DateHolidays from 'date-holidays';
|
||||
import { Event } from './event';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export const eventsRouter = express.Router();
|
||||
|
||||
function getHolidays(req: Request): DateHolidays.HolidaysTypes.Holiday[] {
|
||||
const country = req.query.country as string;
|
||||
const state = req.query.state as string;
|
||||
const year = parseInt(req.query.year as string, 10);
|
||||
const timezone = 'Etc/UTC';
|
||||
|
||||
const dateHolidays = new DateHolidays.default();
|
||||
|
||||
dateHolidays.init(country, state, {
|
||||
timezone: timezone,
|
||||
});
|
||||
|
||||
const holidays = dateHolidays.getHolidays(year);
|
||||
|
||||
return holidays;
|
||||
}
|
||||
|
||||
eventsRouter.get('/next', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const timezone = req.query.timezone as string;
|
||||
|
||||
const holidays = getHolidays(req);
|
||||
|
||||
const events = holidays.map((holiday) => new Event(holiday, timezone));
|
||||
|
||||
const now = DateTime.now().setZone(timezone);
|
||||
|
||||
const nextEvent = events.find(
|
||||
(event) => event.date > now || event.isToday
|
||||
);
|
||||
|
||||
if (!nextEvent) {
|
||||
return res.status(StatusCodes.OK).json(null);
|
||||
}
|
||||
|
||||
return res.status(StatusCodes.OK).json({ responseTime: now, event: nextEvent });
|
||||
} catch (error) {
|
||||
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error });
|
||||
}
|
||||
});
|
||||
|
||||
eventsRouter.get('/future', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const timezone = req.query.timezone as string;
|
||||
|
||||
const holidays = getHolidays(req);
|
||||
|
||||
const events = holidays.map((holiday) => new Event(holiday, timezone));
|
||||
|
||||
const now = DateTime.now().setZone(timezone);
|
||||
|
||||
const futureEvents = events.filter(
|
||||
(event) => event.date > now || event.isToday
|
||||
);
|
||||
|
||||
return res.status(StatusCodes.OK).json({ responseTime: now, events: futureEvents });
|
||||
} catch (error) {
|
||||
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error });
|
||||
}
|
||||
});
|
||||
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist/", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ spec:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: telegram
|
||||
key: bot-token
|
||||
key: bot-token-laundry
|
||||
- name: Telegram__ChatId
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
||||
@@ -192,4 +192,6 @@
|
||||
</Entry>
|
||||
</TypePattern>
|
||||
</Patterns></s:String>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002EMemberReordering_002EMigrations_002ECSharpFileLayoutPatternRemoveIsAttributeUpgrade/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Kaczor/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mqtt/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
45
Environment/Service/Controllers/DeviceController.cs
Normal file
45
Environment/Service/Controllers/DeviceController.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using ChrisKaczor.HomeMonitor.Environment.Service.Data;
|
||||
using ChrisKaczor.HomeMonitor.Environment.Service.Models.Device;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Environment.Service.Controllers;
|
||||
|
||||
[Route("[controller]")]
|
||||
[ApiController]
|
||||
public class DeviceController(Database database, IConfiguration configuration) : ControllerBase
|
||||
{
|
||||
[HttpGet()]
|
||||
public async Task<ActionResult<List<Device>>> GetDevices()
|
||||
{
|
||||
return (await database.GetDevicesAsync()).ToList();
|
||||
}
|
||||
|
||||
[HttpGet("{name}")]
|
||||
public async Task<ActionResult<Device>> GetDevice(string name)
|
||||
{
|
||||
var device = await database.GetDeviceAsync(name);
|
||||
|
||||
if (device == null)
|
||||
return NotFound();
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
[HttpPost()]
|
||||
public async Task<ActionResult> AddDevice(Device device)
|
||||
{
|
||||
HttpContext.Request.Headers.TryGetValue("Authorization", out var authorizationHeader);
|
||||
|
||||
if (authorizationHeader != "Bearer " + configuration["AuthorizationToken"])
|
||||
return Unauthorized();
|
||||
|
||||
var existingDevice = await database.GetDeviceAsync(device.Name);
|
||||
|
||||
if (existingDevice != null)
|
||||
return BadRequest("Device already exists");
|
||||
|
||||
await database.SetDeviceLastUpdatedAsync(device.Name, device.LastUpdated);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using ChrisKaczor.HomeMonitor.Environment.Service.Models;
|
||||
using ChrisKaczor.HomeMonitor.Environment.Service.Models.Device;
|
||||
using ChrisKaczor.HomeMonitor.Environment.Service.Models.Indoor;
|
||||
using Dapper;
|
||||
using DbUp;
|
||||
@@ -80,4 +81,42 @@ public class Database(IConfiguration configuration)
|
||||
|
||||
return await connection.QueryAsync<ReadingsAggregate>(query, new { Start = start, End = end }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Device>> GetDevicesAsync()
|
||||
{
|
||||
await using var connection = CreateConnection();
|
||||
|
||||
var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Environment.Service.Data.Queries.GetDevices.psql");
|
||||
|
||||
return await connection.QueryAsync<Device>(query).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Device?> GetDeviceAsync(string deviceName)
|
||||
{
|
||||
await using var connection = CreateConnection();
|
||||
|
||||
var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Environment.Service.Data.Queries.GetDevice.psql");
|
||||
|
||||
return await connection.QueryFirstOrDefaultAsync<Device>(query, new { Name = deviceName }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> SetDeviceLastUpdatedAsync(string deviceName, DateTimeOffset? lastUpdated)
|
||||
{
|
||||
await using var connection = CreateConnection();
|
||||
|
||||
var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Environment.Service.Data.Queries.SetDeviceLastUpdated.psql");
|
||||
|
||||
var stoppedReporting = await connection.QueryFirstAsync<bool>(query, new { Name = deviceName, LastUpdated = lastUpdated }).ConfigureAwait(false);
|
||||
|
||||
return stoppedReporting;
|
||||
}
|
||||
|
||||
public async Task SetDeviceStoppedReportingAsync(string deviceName, bool stoppedReporting)
|
||||
{
|
||||
await using var connection = CreateConnection();
|
||||
|
||||
var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Environment.Service.Data.Queries.SetDeviceStoppedReporting.psql");
|
||||
|
||||
await connection.ExecuteAsync(query, new { Name = deviceName, StoppedReporting = stoppedReporting }).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
7
Environment/Service/Data/Queries/GetDevice.psql
Normal file
7
Environment/Service/Data/Queries/GetDevice.psql
Normal file
@@ -0,0 +1,7 @@
|
||||
SELECT
|
||||
name AS Name,
|
||||
last_updated AS LastUpdated
|
||||
FROM
|
||||
device
|
||||
WHERE
|
||||
name = @Name
|
||||
5
Environment/Service/Data/Queries/GetDevices.psql
Normal file
5
Environment/Service/Data/Queries/GetDevices.psql
Normal file
@@ -0,0 +1,5 @@
|
||||
SELECT
|
||||
name AS Name,
|
||||
last_updated AS LastUpdated
|
||||
FROM
|
||||
device
|
||||
16
Environment/Service/Data/Queries/SetDeviceLastUpdated.psql
Normal file
16
Environment/Service/Data/Queries/SetDeviceLastUpdated.psql
Normal file
@@ -0,0 +1,16 @@
|
||||
INSERT INTO device(
|
||||
name,
|
||||
last_updated,
|
||||
stopped_reporting
|
||||
)
|
||||
VALUES (
|
||||
@Name,
|
||||
@LastUpdated,
|
||||
false
|
||||
)
|
||||
ON CONFLICT (name)
|
||||
DO UPDATE
|
||||
SET last_updated = EXCLUDED.last_updated,
|
||||
stopped_reporting = false
|
||||
RETURNING
|
||||
(SELECT stopped_reporting FROM device WHERE name = @Name);
|
||||
@@ -0,0 +1,6 @@
|
||||
UPDATE
|
||||
device
|
||||
SET
|
||||
stopped_reporting = @StoppedReporting
|
||||
WHERE
|
||||
name = @Name;
|
||||
5
Environment/Service/Data/Schema/2-Device Table.psql
Normal file
5
Environment/Service/Data/Schema/2-Device Table.psql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE device(
|
||||
name text NOT NULL,
|
||||
last_updated timestamptz NULL,
|
||||
CONSTRAINT device_pk PRIMARY KEY (name)
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE
|
||||
device
|
||||
ADD COLUMN
|
||||
stopped_reporting boolean NOT NULL DEFAULT false;
|
||||
70
Environment/Service/DeviceCheckService.cs
Normal file
70
Environment/Service/DeviceCheckService.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using ChrisKaczor.HomeMonitor.Environment.Service.Data;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Environment.Service;
|
||||
|
||||
public class DeviceCheckService(Database database, IConfiguration configuration, TelegramSender telegramSender) : IHostedService
|
||||
{
|
||||
private Timer? _timer;
|
||||
private TimeSpan _warningInterval;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
WriteLog("DeviceCheckService started");
|
||||
|
||||
_warningInterval = TimeSpan.Parse(configuration["Environment:DeviceWarningInterval"]!);
|
||||
|
||||
var checkInterval = TimeSpan.Parse(configuration["Environment:DeviceCheckInterval"]!);
|
||||
|
||||
_timer = new Timer(_ => DoWork().Wait(cancellationToken), null, TimeSpan.Zero, checkInterval);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task DoWork()
|
||||
{
|
||||
WriteLog("Checking devices started");
|
||||
|
||||
var devices = await database.GetDevicesAsync();
|
||||
|
||||
foreach (var device in devices)
|
||||
{
|
||||
WriteLog($"Checking device: {device.Name}");
|
||||
|
||||
var message = string.Empty;
|
||||
|
||||
WriteLog($"Device {device.Name} last updated: {(device.LastUpdated == null ? "never" : device.LastUpdated.ToString())}");
|
||||
|
||||
if (device.LastUpdated == null)
|
||||
{
|
||||
message = $"Device has never reported: {device.Name}";
|
||||
}
|
||||
else if (DateTime.UtcNow - device.LastUpdated > _warningInterval)
|
||||
{
|
||||
message = $"Device has not reported recently: {device.Name}";
|
||||
}
|
||||
|
||||
if (message.Length > 0)
|
||||
{
|
||||
await database.SetDeviceStoppedReportingAsync(device.Name, true);
|
||||
|
||||
await telegramSender.SendMessageAsync(message);
|
||||
}
|
||||
}
|
||||
|
||||
WriteLog("Checking devices finished");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
WriteLog("DeviceCheckService stopped");
|
||||
|
||||
_timer?.Change(Timeout.Infinite, 0);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void WriteLog(string message)
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,56 @@ Accept: application/json
|
||||
###
|
||||
|
||||
GET {{Environment_HostAddress}}/readings/aggregate?start=2024-03-11T00:00&end=2024-03-12T00:00
|
||||
Accept: application/json
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Environment_HostAddress}}/device
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Environment_HostAddress}}/device/main
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Environment_HostAddress}}/device/test
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
POST {{Environment_HostAddress}}/device
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer test-token
|
||||
Accept: application/json
|
||||
|
||||
{ "name": "test" }
|
||||
|
||||
###
|
||||
|
||||
POST {{Environment_HostAddress}}/device
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer test-token
|
||||
Accept: application/json
|
||||
|
||||
{ }
|
||||
|
||||
###
|
||||
|
||||
POST {{Environment_HostAddress}}/device
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer foo
|
||||
Accept: application/json
|
||||
|
||||
{ "name": "test" }
|
||||
|
||||
###
|
||||
|
||||
POST {{Environment_HostAddress}}/device
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
|
||||
{ "name": "test" }
|
||||
|
||||
###
|
||||
@@ -17,11 +17,13 @@ public class MessageHandler : IHostedService
|
||||
private readonly MqttFactory _mqttFactory;
|
||||
private readonly string _topic;
|
||||
private readonly HubConnection? _hubConnection;
|
||||
private readonly TelegramSender _telegramSender;
|
||||
|
||||
public MessageHandler(IConfiguration configuration, Database database)
|
||||
public MessageHandler(IConfiguration configuration, Database database, TelegramSender telegramSender)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_database = database;
|
||||
_telegramSender = telegramSender;
|
||||
|
||||
_topic = _configuration["Mqtt:Topic"] ?? string.Empty;
|
||||
|
||||
@@ -77,7 +79,16 @@ public class MessageHandler : IHostedService
|
||||
|
||||
await _database.StoreMessageAsync(message);
|
||||
|
||||
var hadStoppedReporting = await _database.SetDeviceLastUpdatedAsync(message.Name, message.Timestamp);
|
||||
|
||||
await SendMessage(message);
|
||||
|
||||
if (hadStoppedReporting)
|
||||
{
|
||||
var telegramMessage = $"Device now reporting: {message.Name}";
|
||||
|
||||
await _telegramSender.SendMessageAsync(telegramMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendMessage(DeviceMessage message)
|
||||
|
||||
7
Environment/Service/Models/Device/Device.cs
Normal file
7
Environment/Service/Models/Device/Device.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace ChrisKaczor.HomeMonitor.Environment.Service.Models.Device;
|
||||
|
||||
public class Device
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public DateTimeOffset? LastUpdated { get; set; }
|
||||
}
|
||||
@@ -19,7 +19,9 @@ public static class Program
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddTransient<Database>();
|
||||
builder.Services.AddTransient<TelegramSender>();
|
||||
builder.Services.AddHostedService<MessageHandler>();
|
||||
builder.Services.AddHostedService<DeviceCheckService>();
|
||||
|
||||
// -- --
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
<None Remove="Data\Queries\GetReadingsAggregate.psql" />
|
||||
<None Remove="Data\Queries\GetReadingsHistoryGrouped.psql" />
|
||||
<None Remove="Data\Queries\GetRecentReadings.psql" />
|
||||
<None Remove="Data\Queries\GetDevices.psql" />
|
||||
<None Remove="Data\Queries\GetDevice.psql" />
|
||||
<None Remove="Data\Queries\SetDeviceLastUpdated.psql" />
|
||||
<None Remove="Data\Schema\1-Initial Schema.psql" />
|
||||
<None Remove="Data\Schema\2-Device Table.psql" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -22,7 +26,13 @@
|
||||
<EmbeddedResource Include="Data\Queries\GetReadingsHistoryGrouped.psql" />
|
||||
<EmbeddedResource Include="Data\Queries\GetRecentReadings.psql" />
|
||||
<EmbeddedResource Include="Data\Queries\CreateReading.psql" />
|
||||
<EmbeddedResource Include="Data\Queries\GetDevices.psql" />
|
||||
<EmbeddedResource Include="Data\Queries\GetDevice.psql" />
|
||||
<EmbeddedResource Include="Data\Queries\SetDeviceStoppedReporting.psql" />
|
||||
<EmbeddedResource Include="Data\Queries\SetDeviceLastUpdated.psql" />
|
||||
<EmbeddedResource Include="Data\Schema\1-Initial Schema.psql" />
|
||||
<EmbeddedResource Include="Data\Schema\3-Device Table - StoppedReporting.psql" />
|
||||
<EmbeddedResource Include="Data\Schema\2-Device Table.psql" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -32,6 +42,7 @@
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.1" />
|
||||
<PackageReference Include="MQTTnet.AspNetCore" Version="4.3.3.952" />
|
||||
<PackageReference Include="RestSharp" Version="112.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
19
Environment/Service/TelegramSender.cs
Normal file
19
Environment/Service/TelegramSender.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using RestSharp;
|
||||
|
||||
namespace ChrisKaczor.HomeMonitor.Environment.Service;
|
||||
|
||||
public class TelegramSender(IConfiguration configuration)
|
||||
{
|
||||
private readonly string _botToken = configuration["Telegram:BotToken"]!;
|
||||
private readonly string _chatId = configuration["Telegram:PersonalChatId"]!;
|
||||
private readonly RestClient _restClient = new();
|
||||
|
||||
public async Task SendMessageAsync(string message)
|
||||
{
|
||||
var encodedMessage = Uri.EscapeDataString(message);
|
||||
|
||||
var restRequest = new RestRequest($"https://api.telegram.org/bot{_botToken}/sendMessage?chat_id={_chatId}&text={encodedMessage}");
|
||||
|
||||
await _restClient.GetAsync(restRequest);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,31 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Mqtt": {
|
||||
"Server": "172.23.10.3"
|
||||
},
|
||||
"Environment": {
|
||||
"Database": {
|
||||
"Host": "localhost",
|
||||
"User": "sa",
|
||||
"Password": "newpassword",
|
||||
"Port": 1433,
|
||||
"Name": "Environment",
|
||||
"TrustServerCertificate": true
|
||||
},
|
||||
"Hub": {
|
||||
"Url": "http://localhost:8080/environment"
|
||||
}
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Mqtt": {
|
||||
"Server": "172.23.10.3"
|
||||
},
|
||||
"Environment": {
|
||||
"Database": {
|
||||
"Host": "localhost",
|
||||
"User": "postgres",
|
||||
"Password": "postgres",
|
||||
"Port": 5432,
|
||||
"Name": "Environment",
|
||||
"TrustServerCertificate": true
|
||||
},
|
||||
"Hub": {
|
||||
"Url": "http://localhost:8080/environment"
|
||||
},
|
||||
"DeviceCheckInterval": "00:00:30",
|
||||
"DeviceWarningInterval": "00:00:10"
|
||||
},
|
||||
"AuthorizationToken": "test-token",
|
||||
"Telegram": {
|
||||
"BotToken": "",
|
||||
"ChatId": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
},
|
||||
"Hub": {
|
||||
"Url": "http://hub-server/environment"
|
||||
}
|
||||
},
|
||||
"DeviceCheckInterval": "12:00:00",
|
||||
"DeviceWarningInterval": "01:00:00"
|
||||
},
|
||||
"Telemetry": {
|
||||
"Endpoint": "http://signoz-otel-collector.platform:4317/"
|
||||
|
||||
@@ -112,6 +112,26 @@ spec:
|
||||
key: password
|
||||
- name: Environment__Hub__Url
|
||||
value: http://hub-service/environment
|
||||
- name: AuthorizationToken
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authorization
|
||||
key: token
|
||||
- name: Telegram__BotToken
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: telegram
|
||||
key: bot-token-home
|
||||
- name: Telegram__GroupChatId
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: telegram
|
||||
key: group-chat-id
|
||||
- name: Telegram__PersonalChatId
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: telegram
|
||||
key: personal-chat-id
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
|
||||
@@ -19,7 +19,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: power-database
|
||||
image: mcr.microsoft.com/mssql/server
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
terminationMessagePath: "/dev/termination-log"
|
||||
terminationMessagePolicy: File
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
@@ -40,11 +40,7 @@ spec:
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
dnsPolicy: ClusterFirst
|
||||
tolerations:
|
||||
- key: weather
|
||||
operator: Equal
|
||||
value: "true"
|
||||
effect: NoSchedule
|
||||
nodeName: weather
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
@@ -114,8 +110,4 @@ spec:
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
dnsPolicy: ClusterFirst
|
||||
tolerations:
|
||||
- key: "weather"
|
||||
operator: Equal
|
||||
value: "true"
|
||||
effect: NoSchedule
|
||||
nodeName: weather
|
||||
@@ -88,7 +88,7 @@ public class WeatherUpdate : WeatherUpdateBase
|
||||
var temperature = Temperature;
|
||||
var humidity = Humidity;
|
||||
|
||||
if (temperature.IsBetween(80, 100) && humidity.IsBetween(40, 100))
|
||||
if (temperature > 80 && humidity > 40)
|
||||
{
|
||||
HeatIndex = -42.379m + 2.04901523m * temperature + 10.14333127m * humidity -
|
||||
.22475541m * temperature * humidity - .00683783m * temperature * temperature -
|
||||
|
||||
@@ -19,7 +19,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: weather-database
|
||||
image: mcr.microsoft.com/mssql/server
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
terminationMessagePath: "/dev/termination-log"
|
||||
terminationMessagePolicy: File
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# build stage
|
||||
FROM node:lts-alpine as build-stage
|
||||
FROM node:lts-alpine AS build-stage
|
||||
RUN npm install -g corepack@latest
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
@@ -8,9 +9,12 @@ COPY . .
|
||||
RUN pnpm run build
|
||||
|
||||
# production stage
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
FROM nginx:stable-alpine AS production-stage
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
COPY ./nginx/entrypoint.sh ./docker-entrypoint.d/entrypoint.sh
|
||||
RUN chmod +x ./docker-entrypoint.d/entrypoint.sh
|
||||
RUN echo "daemon off;" >> /etc/nginx/nginx.conf
|
||||
CMD ["nginx"]
|
||||
5
WebDisplay/components.d.ts
vendored
5
WebDisplay/components.d.ts
vendored
@@ -8,16 +8,21 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Almanac: typeof import('./src/components/Almanac.vue')['default']
|
||||
CalendarAgenda: typeof import('./src/components/CalendarAgenda.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']
|
||||
Indoor: typeof import('./src/components/Indoor.vue')['default']
|
||||
IndoorSummary: typeof import('./src/components/IndoorSummary.vue')['default']
|
||||
LongPressButton: typeof import('./src/components/LongPressButton.vue')['default']
|
||||
NationalDays: typeof import('./src/components/NationalDays.vue')['default']
|
||||
PressureTrendArrow: typeof import('./src/components/PressureTrendArrow.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
TimeRange: typeof import('./src/components/TimeRange.vue')['default']
|
||||
ValueChart: typeof import('./src/components/ValueChart.vue')['default']
|
||||
WeatherSummary: typeof import('./src/components/WeatherSummary.vue')['default']
|
||||
WindDirectionArrow: typeof import('./src/components/WindDirectionArrow.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,66 +3,94 @@ name: $(Rev:r)
|
||||
pr: none
|
||||
|
||||
trigger:
|
||||
batch: 'true'
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
paths:
|
||||
include:
|
||||
- WebDisplay
|
||||
batch: 'true'
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
paths:
|
||||
include:
|
||||
- WebDisplay
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
jobs:
|
||||
- job: Build
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: Docker@0
|
||||
displayName: 'Build an image'
|
||||
inputs:
|
||||
containerregistrytype: 'Container Registry'
|
||||
dockerRegistryConnection: 'Docker Hub'
|
||||
dockerFile: 'WebDisplay/Dockerfile'
|
||||
imageName: 'ckaczor/home-monitor-web-display:$(Build.BuildNumber)'
|
||||
includeLatestTag: true
|
||||
- task: Docker@0
|
||||
displayName: 'Push an image'
|
||||
inputs:
|
||||
containerregistrytype: 'Container Registry'
|
||||
dockerRegistryConnection: 'Docker Hub'
|
||||
action: 'Push an image'
|
||||
imageName: 'ckaczor/home-monitor-web-display:$(Build.BuildNumber)'
|
||||
includeLatestTag: true
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: 'sed -i s/#BUILD_BUILDNUMBER#/$BUILD_BUILDNUMBER/ WebDisplay/deploy/manifest.yaml'
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
PathtoPublish: 'WebDisplay/deploy/manifest.yaml'
|
||||
ArtifactName: 'Manifest'
|
||||
publishLocation: 'Container'
|
||||
- stage: Build
|
||||
jobs:
|
||||
- job: Build
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: Docker@2
|
||||
displayName: 'Build an image'
|
||||
inputs:
|
||||
command: 'build'
|
||||
containerRegistry: 'Docker Hub'
|
||||
dockerFile: 'WebDisplay/Dockerfile'
|
||||
repository: 'ckaczor/home-monitor-web-display'
|
||||
tags: '$(Build.BuildNumber),latest'
|
||||
- task: Docker@2
|
||||
displayName: 'Push an image'
|
||||
inputs:
|
||||
command: 'push'
|
||||
containerRegistry: 'Docker Hub'
|
||||
repository: 'ckaczor/home-monitor-web-display'
|
||||
tags: '$(Build.BuildNumber),latest'
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: 'sed -i s/#BUILD_BUILDNUMBER#/$BUILD_BUILDNUMBER/ WebDisplay/deploy/manifest.yaml'
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
PathtoPublish: 'WebDisplay/deploy/manifest.yaml'
|
||||
ArtifactName: 'Manifest'
|
||||
publishLocation: 'Container'
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: 'sed -i s/#BUILD_BUILDNUMBER#/$BUILD_BUILDNUMBER/ WebDisplay/deploy/manifest-internal.yaml'
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
PathtoPublish: 'WebDisplay/deploy/manifest-internal.yaml'
|
||||
ArtifactName: 'Manifest-Internal'
|
||||
publishLocation: 'Container'
|
||||
|
||||
- stage: Deploy
|
||||
jobs:
|
||||
- job: Deploy
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: DownloadBuildArtifacts@0
|
||||
inputs:
|
||||
artifactName: 'Manifest'
|
||||
buildType: 'current'
|
||||
downloadType: 'single'
|
||||
downloadPath: '$(System.ArtifactsDirectory)'
|
||||
- task: Kubernetes@1
|
||||
inputs:
|
||||
connectionType: 'Kubernetes Service Connection'
|
||||
kubernetesServiceEndpoint: 'Kubernetes'
|
||||
namespace: 'home-monitor'
|
||||
command: 'apply'
|
||||
useConfigurationFile: true
|
||||
configuration: '$(System.ArtifactsDirectory)/Manifest/manifest.yaml'
|
||||
secretType: 'dockerRegistry'
|
||||
containerRegistryType: 'Container Registry'
|
||||
- stage: Deploy
|
||||
jobs:
|
||||
- job: Deploy
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: DownloadBuildArtifacts@0
|
||||
inputs:
|
||||
artifactName: 'Manifest'
|
||||
buildType: 'current'
|
||||
downloadType: 'single'
|
||||
downloadPath: '$(System.ArtifactsDirectory)'
|
||||
- task: Kubernetes@1
|
||||
inputs:
|
||||
connectionType: 'Kubernetes Service Connection'
|
||||
kubernetesServiceEndpoint: 'Kubernetes'
|
||||
namespace: 'home-monitor'
|
||||
command: 'apply'
|
||||
useConfigurationFile: true
|
||||
configuration: '$(System.ArtifactsDirectory)/Manifest/manifest.yaml'
|
||||
secretType: 'dockerRegistry'
|
||||
containerRegistryType: 'Container Registry'
|
||||
- job: Deploy_Internal
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: DownloadBuildArtifacts@0
|
||||
inputs:
|
||||
artifactName: 'Manifest-Internal'
|
||||
buildType: 'current'
|
||||
downloadType: 'single'
|
||||
downloadPath: '$(System.ArtifactsDirectory)'
|
||||
- task: Kubernetes@1
|
||||
inputs:
|
||||
connectionType: 'Kubernetes Service Connection'
|
||||
kubernetesServiceEndpoint: 'Kubernetes'
|
||||
namespace: 'home-monitor'
|
||||
command: 'apply'
|
||||
useConfigurationFile: true
|
||||
configuration: '$(System.ArtifactsDirectory)/Manifest-Internal/manifest-internal.yaml'
|
||||
secretType: 'dockerRegistry'
|
||||
containerRegistryType: 'Container Registry'
|
||||
|
||||
105
WebDisplay/deploy/manifest-internal.yaml
Normal file
105
WebDisplay/deploy/manifest-internal.yaml
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
kind: Deployment
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
name: display-internal
|
||||
namespace: home-monitor
|
||||
labels:
|
||||
app: display-internal
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: display-internal
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: display-internal
|
||||
spec:
|
||||
containers:
|
||||
- name: display-internal
|
||||
image: ckaczor/home-monitor-web-display:#BUILD_BUILDNUMBER#
|
||||
terminationMessagePath: "/dev/termination-log"
|
||||
terminationMessagePolicy: File
|
||||
imagePullPolicy: Always
|
||||
securityContext:
|
||||
privileged: true
|
||||
env:
|
||||
- name: API_PREFIX
|
||||
value: http://172.23.10.3
|
||||
- name: HOME_ASSISTANT_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: display-internal-config
|
||||
key: HOME_ASSISTANT_URL
|
||||
- name: HOME_ASSISTANT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: display-internal-config
|
||||
key: HOME_ASSISTANT_TOKEN
|
||||
- name: GARAGE_DEVICE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: display-internal-config
|
||||
key: GARAGE_DEVICE
|
||||
- name: ALARM_DEVICE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: display-internal-config
|
||||
key: ALARM_DEVICE
|
||||
- name: CALENDAR_EMBED_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: display-internal-config
|
||||
key: CALENDAR_EMBED_URL
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
dnsPolicy: ClusterFirst
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: kubernetes
|
||||
schedulerName: default-scheduler
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: display-internal
|
||||
spec:
|
||||
ports:
|
||||
- name: client
|
||||
port: 9001
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: display-internal
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: traefik
|
||||
creationTimestamp: null
|
||||
name: display-internal
|
||||
namespace: home-monitor
|
||||
spec:
|
||||
entryPoints:
|
||||
- display
|
||||
routes:
|
||||
- kind: Rule
|
||||
match: PathPrefix(`/`)
|
||||
priority: 101
|
||||
services:
|
||||
- kind: Service
|
||||
name: display-internal
|
||||
namespace: home-monitor
|
||||
port: 9001
|
||||
---
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: display-internal
|
||||
namespace: home-monitor
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- /
|
||||
@@ -24,6 +24,19 @@ spec:
|
||||
imagePullPolicy: Always
|
||||
securityContext:
|
||||
privileged: true
|
||||
env:
|
||||
- name: API_PREFIX
|
||||
value:
|
||||
- name: HOME_ASSISTANT_URL
|
||||
value:
|
||||
- name: HOME_ASSISTANT_TOKEN
|
||||
value:
|
||||
- name: GARAGE_DEVICE
|
||||
value:
|
||||
- name: ALARM_DEVICE
|
||||
value:
|
||||
- name: CALENDAR_EMBED_URL
|
||||
value:
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
dnsPolicy: ClusterFirst
|
||||
|
||||
7
WebDisplay/nginx/entrypoint.sh
Normal file
7
WebDisplay/nginx/entrypoint.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
sed -i -e "s~#API_PREFIX#~$API_PREFIX~g" /usr/share/nginx/html/assets/*
|
||||
sed -i -e "s~#HOME_ASSISTANT_URL#~$HOME_ASSISTANT_URL~g" /usr/share/nginx/html/assets/*
|
||||
sed -i -e "s~#HOME_ASSISTANT_TOKEN#~$HOME_ASSISTANT_TOKEN~g" /usr/share/nginx/html/assets/*
|
||||
sed -i -e "s~#GARAGE_DEVICE#~$GARAGE_DEVICE~g" /usr/share/nginx/html/assets/*
|
||||
sed -i -e "s~#ALARM_DEVICE#~$ALARM_DEVICE~g" /usr/share/nginx/html/assets/*
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "web-display",
|
||||
"version": "1.0.0",
|
||||
"packageManager": "pnpm@10.29.3",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_OPTIONS='--no-warnings' vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
@@ -9,41 +10,42 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.0.96",
|
||||
"@microsoft/signalr": "^8.0.0",
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@types/suncalc": "^1.9.2",
|
||||
"@vuepic/vue-datepicker": "^8.2.0",
|
||||
"apexcharts": "^3.46.0",
|
||||
"axios": "^1.6.7",
|
||||
"core-js": "^3.34.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"pinia": "^2.1.7",
|
||||
"roboto-fontface": "*",
|
||||
"@vuepic/vue-datepicker": "^8.8.1",
|
||||
"apexcharts": "^4.1.0",
|
||||
"axios": "^1.7.9",
|
||||
"core-js": "^3.39.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"home-assistant-js-websocket": "^9.4.0",
|
||||
"pinia": "^2.3.0",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"suncalc": "^1.9.0",
|
||||
"vue": "^3.3.0",
|
||||
"vue3-apexcharts": "^1.5.2",
|
||||
"vuetify": "^3.0.0"
|
||||
"vue": "^3.5.13",
|
||||
"vue3-apexcharts": "^1.8.0",
|
||||
"vuetify": "^3.7.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.23.0",
|
||||
"@types/node": "^20.10.0",
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@babel/types": "^7.26.3",
|
||||
"@types/node": "^20.17.9",
|
||||
"@typescript-eslint/parser": "8.17.0",
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
"@vue/eslint-config-typescript": "^14.1.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-n": "^16.4.0",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"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",
|
||||
"eslint-plugin-promise": "^6.6.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"sass": "^1.82.0",
|
||||
"typescript": "5.6.2",
|
||||
"unplugin-fonts": "^1.3.1",
|
||||
"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"
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-vuetify": "^2.0.4",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
5444
WebDisplay/pnpm-lock.yaml
generated
5444
WebDisplay/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,67 +1,7 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-app-bar
|
||||
title="Home Monitor"
|
||||
color="primary">
|
||||
<template v-slot:prepend>
|
||||
<v-app-bar-nav-icon
|
||||
v-show="!mdAndUp"
|
||||
@click="drawer = !drawer"></v-app-bar-nav-icon>
|
||||
</template>
|
||||
</v-app-bar>
|
||||
|
||||
<v-navigation-drawer
|
||||
mobile-breakpoint="md"
|
||||
:expand-on-hover="mdAndUp"
|
||||
:rail="mdAndUp"
|
||||
:model-value="mdAndUp ? true : drawer">
|
||||
<v-list
|
||||
density="compact"
|
||||
nav>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-information-outline"
|
||||
title="Current"
|
||||
to="/"
|
||||
@click="drawer = false">
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-chart-box-outline"
|
||||
title="Summary"
|
||||
to="summary"
|
||||
@click="drawer = false">
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-sun-thermometer"
|
||||
title="Outdoor"
|
||||
to="outdoor"
|
||||
@click="drawer = false">
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-home-analytics"
|
||||
title="Indoor"
|
||||
to="indoor"
|
||||
@click="drawer = false">
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-home-lightning-bolt-outline"
|
||||
title="Power"
|
||||
to="power"
|
||||
@click="drawer = false">
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
<router-view></router-view>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useDisplay } from 'vuetify';
|
||||
|
||||
const drawer = ref(false);
|
||||
|
||||
const { mdAndUp } = useDisplay();
|
||||
</script>
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
155
WebDisplay/src/components/CalendarAgenda.vue
Normal file
155
WebDisplay/src/components/CalendarAgenda.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useCalendarStore } from '@/stores/calendarStore';
|
||||
import { format, startOfDay, endOfDay } from 'date-fns';
|
||||
import { setNextDayTimer } from '@/nextDayTimer';
|
||||
import CalendarDay from '@/models/calendar/calendar-day';
|
||||
|
||||
const props = defineProps(['days', 'refreshInterval']);
|
||||
|
||||
const calendarStore = useCalendarStore();
|
||||
|
||||
const calendarDays = ref([] as CalendarDay[]);
|
||||
const calendarReady = ref(false);
|
||||
|
||||
function loadCalendar() {
|
||||
const newCalendarDays = [] as CalendarDay[];
|
||||
|
||||
calendarStore.getUpcoming(props.days, true).then((upcoming) => {
|
||||
const currentDay = startOfDay(new Date());
|
||||
|
||||
for (let i = 0; i < props.days; i++) {
|
||||
const day = new Date(currentDay);
|
||||
day.setDate(day.getDate() + i);
|
||||
|
||||
const entries = upcoming
|
||||
.filter((entry) => {
|
||||
const entryStart = startOfDay(entry.start);
|
||||
const entryEnd = endOfDay(entry.end);
|
||||
|
||||
if (entry.isAllDay) {
|
||||
return day > entryStart && day < entryEnd;
|
||||
}
|
||||
|
||||
return day >= entryStart && day <= entryEnd;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.isHoliday == b.isHoliday) {
|
||||
return a.summary.localeCompare(b.summary);
|
||||
}
|
||||
|
||||
return (b.isHoliday ? 1 : 0) - (a.isHoliday ? 1 : 0);
|
||||
});
|
||||
|
||||
newCalendarDays.push(new CalendarDay(day, entries));
|
||||
}
|
||||
|
||||
calendarDays.value = newCalendarDays;
|
||||
|
||||
calendarReady.value = true;
|
||||
|
||||
setNextDayTimer(loadCalendar, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
loadCalendar();
|
||||
|
||||
setInterval(loadCalendar, props.refreshInterval);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="calendar"
|
||||
v-if="calendarReady">
|
||||
<div class="calendar-header">
|
||||
{{ 'Next ' + days + ' Days' }}
|
||||
</div>
|
||||
<ul class="calendar-day-list">
|
||||
<li
|
||||
class="calendar-day-item"
|
||||
v-for="calendarDay in calendarDays">
|
||||
<div>
|
||||
<div class="calendar-day-item-header">
|
||||
<span class="calendar-day-item-number">
|
||||
{{ format(calendarDay.date, 'd') }}
|
||||
</span>
|
||||
<span class="calendar-day-item-name">
|
||||
{{ format(calendarDay.date, 'EEEE') }}
|
||||
</span>
|
||||
</div>
|
||||
<ul
|
||||
class="calendar-entry"
|
||||
v-for="calendarEntry in calendarDay.entries"
|
||||
:class="{ 'calendar-holiday': calendarEntry.isHoliday }">
|
||||
<span>
|
||||
{{ calendarEntry.summary }}
|
||||
<span v-if="!calendarEntry.isAllDay">@ {{ format(calendarEntry.start, 'p') }}</span>
|
||||
</span>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.calendar {
|
||||
background-color: #121212;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
font-size: 1.15em;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-day-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.calendar-day-item-number {
|
||||
font-size: 1.25em;
|
||||
padding-right: 0.5em;
|
||||
width: 2rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.calendar-day-item-name {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.calendar-day-list {
|
||||
margin-left: 10px;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.calendar-day-item:not(:last-child) {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.calendar-day-item:first-of-type {
|
||||
color: #c75ec7;
|
||||
}
|
||||
|
||||
.calendar-day-item:not(:first-child) {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.calendar-entry {
|
||||
color: #ebebeb;
|
||||
padding-left: 2em;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.calendar-holiday {
|
||||
color: #5e83c7;
|
||||
}
|
||||
</style>
|
||||
@@ -1,28 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useWeatherStore } from '@/stores/weatherStore';
|
||||
import { ConvertPascalToInchesOfMercury } from '@/pressureConverter';
|
||||
import { ShortenWindDirection } from '@/windFormatter';
|
||||
import PressureTrendArrow from './PressureTrendArrow.vue';
|
||||
|
||||
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>
|
||||
@@ -65,13 +47,10 @@
|
||||
<tr>
|
||||
<td className="weather-current-header">Pressure</td>
|
||||
<td colspan="3">
|
||||
{{ weatherStore.current?.Pressure && ConvertPascalToInchesOfMercury(weatherStore.current?.Pressure)?.toFixed(2) }}"
|
||||
<span
|
||||
{{ weatherStore.current?.Pressure && (weatherStore.current?.Pressure / 100).toFixed(2) }} mbar
|
||||
<PressureTrendArrow
|
||||
class="pressure-trend-arrow"
|
||||
:class="rotationClass(weatherStore.current?.PressureDifferenceThreeHour)"
|
||||
:title="'3 Hour Change: ' + weatherStore.current?.PressureDifferenceThreeHour?.toFixed(1)">
|
||||
➜
|
||||
</span>
|
||||
:pressureDifference="weatherStore.current?.PressureDifferenceThreeHour" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -115,25 +94,6 @@
|
||||
}
|
||||
|
||||
.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);
|
||||
scale: 1.25;
|
||||
}
|
||||
</style>
|
||||
|
||||
82
WebDisplay/src/components/LongPressButton.vue
Normal file
82
WebDisplay/src/components/LongPressButton.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const emit = defineEmits(['longPress']);
|
||||
const props = defineProps(['duration', 'increment', 'progressSize']);
|
||||
|
||||
const current = ref(0 as number);
|
||||
const loading = ref(false);
|
||||
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
function startProgress(pointerEvent: PointerEvent) {
|
||||
(pointerEvent.target as HTMLButtonElement).setPointerCapture(pointerEvent.pointerId);
|
||||
|
||||
loading.value = true;
|
||||
|
||||
incrementProgress();
|
||||
|
||||
interval = setInterval(incrementProgress, props.increment);
|
||||
}
|
||||
|
||||
function stopProgress() {
|
||||
clearInterval(interval);
|
||||
|
||||
loading.value = false;
|
||||
current.value = 0;
|
||||
}
|
||||
|
||||
function incrementProgress() {
|
||||
current.value = current.value + props.increment;
|
||||
|
||||
if (current.value >= props.duration) {
|
||||
emit('longPress');
|
||||
|
||||
stopProgress();
|
||||
}
|
||||
}
|
||||
|
||||
function progress() {
|
||||
if (current.value >= props.duration) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return (current.value / props.duration) * 100;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="long-press-button"
|
||||
:class="{ loading: loading }"
|
||||
@pointerdown="startProgress"
|
||||
@pointerup="stopProgress"
|
||||
@pointercancel="stopProgress">
|
||||
<span v-show="!loading">
|
||||
<slot name="default"></slot>
|
||||
</span>
|
||||
<v-progress-circular
|
||||
v-if="loading"
|
||||
:size="props.progressSize"
|
||||
:model-value="progress()"></v-progress-circular>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.long-press-button {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.v-progress-circular__underlay {
|
||||
stroke: #ebebeb !important;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
106
WebDisplay/src/components/NationalDays.vue
Normal file
106
WebDisplay/src/components/NationalDays.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useCalendarStore } from '@/stores/calendarStore';
|
||||
import { setNextDayTimer } from '@/nextDayTimer';
|
||||
import NationalDayEntry from '@/models/calendar/national-day';
|
||||
|
||||
const dialog = ref(false);
|
||||
const selectedNationalDay = ref({} as NationalDayEntry);
|
||||
|
||||
const calendarStore = useCalendarStore();
|
||||
|
||||
const nationalDays = ref([] as NationalDayEntry[]);
|
||||
const nationalDaysReady = ref(false);
|
||||
|
||||
function loadNationalDays() {
|
||||
calendarStore.getNationalDays().then((data) => {
|
||||
nationalDays.value = data;
|
||||
|
||||
nationalDaysReady.value = true;
|
||||
|
||||
setNextDayTimer(loadNationalDays, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
function onNationalDayClick(nationalDay: NationalDayEntry) {
|
||||
selectedNationalDay.value = nationalDay;
|
||||
dialog.value = true;
|
||||
}
|
||||
|
||||
loadNationalDays();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="national-days"
|
||||
v-if="nationalDaysReady">
|
||||
<div class="national-days-header">
|
||||
{{ 'National Days (' + nationalDays.length + ')' }}
|
||||
</div>
|
||||
<ul class="national-days-list">
|
||||
<li
|
||||
class="national-day"
|
||||
v-for="nationalDay in nationalDays"
|
||||
@click="onNationalDayClick(nationalDay)">
|
||||
<span v-html="nationalDay.name"></span>
|
||||
<v-icon
|
||||
class="national-day-arrow"
|
||||
icon="mdi-menu-right" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
width="50%"
|
||||
theme="dark"
|
||||
opacity="0.85"
|
||||
scrim="black">
|
||||
<v-card>
|
||||
<template v-slot:title>
|
||||
<span v-html="selectedNationalDay.name"></span>
|
||||
</template>
|
||||
<template v-slot:text>
|
||||
<span v-html="selectedNationalDay.excerpt"></span>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
class="ms-auto"
|
||||
text="Close"
|
||||
@click="dialog = false"></v-btn>
|
||||
</template>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.national-days {
|
||||
background-color: #121212;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.national-days-header {
|
||||
font-size: 1.15em;
|
||||
padding-bottom: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.national-days-list {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.national-day {
|
||||
list-style-type: none;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.national-day-arrow {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
52
WebDisplay/src/components/PressureTrendArrow.vue
Normal file
52
WebDisplay/src/components/PressureTrendArrow.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps(['pressureDifference']);
|
||||
|
||||
function 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>
|
||||
<span
|
||||
class="pressure-trend-arrow"
|
||||
:class="rotationClass(props.pressureDifference)"
|
||||
:title="'3 Hour Change: ' + props.pressureDifference.toFixed(1)">
|
||||
➝
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pressure-trend-arrow {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.down-high {
|
||||
transform: rotate(60deg);
|
||||
}
|
||||
|
||||
.down-low {
|
||||
transform: rotate(25deg);
|
||||
}
|
||||
|
||||
.up-high {
|
||||
transform: rotate(-60deg);
|
||||
}
|
||||
|
||||
.up-low {
|
||||
transform: rotate(-25deg);
|
||||
}
|
||||
</style>
|
||||
128
WebDisplay/src/components/WindDirectionArrow.vue
Normal file
128
WebDisplay/src/components/WindDirectionArrow.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<script lang="ts" setup>
|
||||
import WindDirection from '@/models/weather/wind-direction';
|
||||
|
||||
const props = defineProps(['windDirection']);
|
||||
|
||||
function rotationClass(windDirection: WindDirection | undefined) {
|
||||
switch (windDirection) {
|
||||
case WindDirection.None:
|
||||
return 'None';
|
||||
case WindDirection.North:
|
||||
return 'N';
|
||||
case WindDirection.East:
|
||||
return 'E';
|
||||
case WindDirection.South:
|
||||
return 'S';
|
||||
case WindDirection.West:
|
||||
return 'W';
|
||||
case WindDirection.NorthEast:
|
||||
return 'NE';
|
||||
case WindDirection.SouthEast:
|
||||
return 'SE';
|
||||
case WindDirection.SouthWest:
|
||||
return 'SW';
|
||||
case WindDirection.NorthWest:
|
||||
return 'NW';
|
||||
case WindDirection.NorthNorthEast:
|
||||
return 'NNE';
|
||||
case WindDirection.EastNorthEast:
|
||||
return 'ENE';
|
||||
case WindDirection.EastSouthEast:
|
||||
return 'ESE';
|
||||
case WindDirection.SouthSouthEast:
|
||||
return 'SSE';
|
||||
case WindDirection.SouthSouthWest:
|
||||
return 'SSW';
|
||||
case WindDirection.WestSouthWest:
|
||||
return 'WSW';
|
||||
case WindDirection.WestNorthWest:
|
||||
return 'WNW';
|
||||
case WindDirection.NorthNorthWest:
|
||||
return 'NNW';
|
||||
}
|
||||
return windDirection!.toString();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="wind-direction-arrow"
|
||||
:class="rotationClass(props.windDirection)"
|
||||
:title="props.windDirection">
|
||||
➳
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wind-direction-arrow {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.None {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.N {
|
||||
transform: rotate(calc(-90deg + 0deg));
|
||||
}
|
||||
|
||||
.E {
|
||||
transform: rotate(calc(-90deg + 90deg));
|
||||
}
|
||||
|
||||
.S {
|
||||
transform: rotate(calc(-90deg + 180deg));
|
||||
}
|
||||
|
||||
.W {
|
||||
transform: rotate(calc(-90deg + 270deg));
|
||||
}
|
||||
|
||||
.NE {
|
||||
transform: rotate(calc(-90deg + 45deg));
|
||||
}
|
||||
|
||||
.SE {
|
||||
transform: rotate(calc(-90deg + 135deg));
|
||||
}
|
||||
|
||||
.SW {
|
||||
transform: rotate(calc(-90deg + 225deg));
|
||||
}
|
||||
|
||||
.NW {
|
||||
transform: rotate(calc(-90deg + 315deg));
|
||||
}
|
||||
|
||||
.NNE {
|
||||
transform: rotate(calc(-90deg + 22.5deg));
|
||||
}
|
||||
|
||||
.ENE {
|
||||
transform: rotate(calc(-90deg + 67.5deg));
|
||||
}
|
||||
|
||||
.ESE {
|
||||
transform: rotate(calc(-90deg + 112.5deg));
|
||||
}
|
||||
|
||||
.SSE {
|
||||
transform: rotate(calc(-90deg + 157.5deg));
|
||||
}
|
||||
|
||||
.SSW {
|
||||
transform: rotate(calc(-90deg + 202.5deg));
|
||||
}
|
||||
|
||||
.WSW {
|
||||
transform: rotate(calc(-90deg + 247.5deg));
|
||||
}
|
||||
|
||||
.WNW {
|
||||
transform: rotate(calc(-90deg + 292.5deg));
|
||||
}
|
||||
|
||||
.NNW {
|
||||
transform: rotate(calc(-90deg + 337.5deg));
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"API_PREFIX": ""
|
||||
}
|
||||
15
WebDisplay/src/dateReviver.ts
Normal file
15
WebDisplay/src/dateReviver.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function dateReviver(key: string, value: any): any {
|
||||
if (key.indexOf('Timestamp') == -1) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.match(/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)?$/)) {
|
||||
const date = new Date(value);
|
||||
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -1,7 +1,21 @@
|
||||
import config from './config.json';
|
||||
|
||||
export default class Environment {
|
||||
public static getUrlPrefix(): string {
|
||||
return config.API_PREFIX;
|
||||
return '#API_PREFIX#';
|
||||
}
|
||||
|
||||
public static getHomeAssistantUrl(): string {
|
||||
return '#HOME_ASSISTANT_URL#';
|
||||
}
|
||||
|
||||
public static getHomeAssistantToken(): string {
|
||||
return '#HOME_ASSISTANT_TOKEN#';
|
||||
}
|
||||
|
||||
public static getGarageDevice(): string {
|
||||
return '#GARAGE_DEVICE#';
|
||||
}
|
||||
|
||||
public static getAlarmDevice(): string {
|
||||
return '#ALARM_DEVICE#';
|
||||
}
|
||||
}
|
||||
|
||||
11
WebDisplay/src/models/calendar/calendar-day.ts
Normal file
11
WebDisplay/src/models/calendar/calendar-day.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import CalendarEntry from './calendar-entry';
|
||||
|
||||
export default class CalendarDay {
|
||||
date: Date;
|
||||
entries: CalendarEntry[];
|
||||
|
||||
constructor(date: Date, entries: CalendarEntry[]) {
|
||||
this.date = date;
|
||||
this.entries = entries;
|
||||
}
|
||||
}
|
||||
7
WebDisplay/src/models/calendar/calendar-entry.ts
Normal file
7
WebDisplay/src/models/calendar/calendar-entry.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default interface CalendarEntry {
|
||||
summary: string;
|
||||
isAllDay: boolean;
|
||||
start: Date;
|
||||
end: Date;
|
||||
isHoliday: boolean;
|
||||
}
|
||||
6
WebDisplay/src/models/calendar/national-day.ts
Normal file
6
WebDisplay/src/models/calendar/national-day.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default interface NationalDayEntry {
|
||||
name: string;
|
||||
url: string;
|
||||
excerpt: string;
|
||||
type: string;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export default class PowerStatus {
|
||||
Timestamp: Date | undefined;
|
||||
Generation: number = 0;
|
||||
Consumption: number = 0;
|
||||
}
|
||||
|
||||
12
WebDisplay/src/nextDayTimer.ts
Normal file
12
WebDisplay/src/nextDayTimer.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { addDays, startOfDay } from 'date-fns';
|
||||
|
||||
type CallbackFunction = () => void;
|
||||
|
||||
export function setNextDayTimer(callback: CallbackFunction, offset: number): void {
|
||||
const now = new Date();
|
||||
const startOfNextDay = startOfDay(addDays(now, 1));
|
||||
|
||||
const millisecondsUntilNextDay = startOfNextDay.getTime() - now.getTime() + offset;
|
||||
|
||||
setTimeout(callback, millisecondsUntilNextDay);
|
||||
}
|
||||
@@ -1,104 +1,68 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<v-container
|
||||
fluid
|
||||
class="container">
|
||||
<v-card class="current-weather">
|
||||
<CurrentWeather></CurrentWeather>
|
||||
</v-card>
|
||||
<v-card class="almanac">
|
||||
<Almanac></Almanac>
|
||||
</v-card>
|
||||
<v-card class="current-power">
|
||||
<CurrentPower></CurrentPower>
|
||||
</v-card>
|
||||
<v-card class="current-laundry-status">
|
||||
<CurrentLaundryStatus></CurrentLaundryStatus>
|
||||
</v-card>
|
||||
<v-card class="upstairs">
|
||||
<Indoor
|
||||
title="Upstairs"
|
||||
deviceName="main"></Indoor>
|
||||
</v-card>
|
||||
<v-card class="downstairs">
|
||||
<Indoor
|
||||
title="Downstairs"
|
||||
deviceName="basement"></Indoor>
|
||||
</v-card>
|
||||
</v-container>
|
||||
<v-app>
|
||||
<v-app-bar
|
||||
title="Home Monitor"
|
||||
color="primary">
|
||||
<template v-slot:prepend>
|
||||
<v-app-bar-nav-icon
|
||||
v-show="!mdAndUp"
|
||||
@click="drawer = !drawer"></v-app-bar-nav-icon>
|
||||
</template>
|
||||
</v-app-bar>
|
||||
|
||||
<v-navigation-drawer
|
||||
mobile-breakpoint="md"
|
||||
:expand-on-hover="mdAndUp"
|
||||
:rail="mdAndUp"
|
||||
:model-value="mdAndUp ? true : drawer">
|
||||
<v-list
|
||||
density="compact"
|
||||
nav>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-information-outline"
|
||||
title="Current"
|
||||
:active="$route.path === '/'"
|
||||
to="/"
|
||||
@click="drawer = false">
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-chart-box-outline"
|
||||
title="Summary"
|
||||
to="summary"
|
||||
@click="drawer = false">
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-sun-thermometer"
|
||||
title="Outdoor"
|
||||
to="outdoor"
|
||||
@click="drawer = false">
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-home-analytics"
|
||||
title="Indoor"
|
||||
to="indoor"
|
||||
@click="drawer = false">
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-home-lightning-bolt-outline"
|
||||
title="Power"
|
||||
to="power"
|
||||
@click="drawer = false">
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
height: 100%;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useDisplay } from 'vuetify';
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 350px 210px 1fr;
|
||||
grid-template-rows: repeat(3, max-content);
|
||||
gap: 15px 15px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'current-weather almanac current-laundry-status'
|
||||
'current-weather almanac current-power'
|
||||
'upstairs downstairs .';
|
||||
}
|
||||
}
|
||||
const drawer = ref(false);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 180px);
|
||||
grid-template-rows: repeat(2, max-content);
|
||||
gap: 15px 15px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'current-weather current-weather almanac almanac'
|
||||
'current-power current-laundry-status upstairs downstairs';
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
grid-template-rows: repeat(6, max-content);
|
||||
gap: 10px 0px;
|
||||
grid-template-areas:
|
||||
'current-weather'
|
||||
'almanac'
|
||||
'current-power'
|
||||
'current-laundry-status'
|
||||
'upstairs'
|
||||
'downstairs';
|
||||
}
|
||||
}
|
||||
|
||||
.current-weather {
|
||||
grid-area: current-weather;
|
||||
}
|
||||
|
||||
.almanac {
|
||||
grid-area: almanac;
|
||||
}
|
||||
|
||||
.current-power {
|
||||
grid-area: current-power;
|
||||
}
|
||||
|
||||
.current-laundry-status {
|
||||
grid-area: current-laundry-status;
|
||||
}
|
||||
|
||||
.upstairs {
|
||||
grid-area: upstairs;
|
||||
}
|
||||
|
||||
.downstairs {
|
||||
grid-area: downstairs;
|
||||
}
|
||||
</style>
|
||||
const { mdAndUp } = useDisplay();
|
||||
</script>
|
||||
|
||||
104
WebDisplay/src/pages/index/index.vue
Normal file
104
WebDisplay/src/pages/index/index.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<v-container
|
||||
fluid
|
||||
class="container">
|
||||
<v-card class="current-weather">
|
||||
<CurrentWeather></CurrentWeather>
|
||||
</v-card>
|
||||
<v-card class="almanac">
|
||||
<Almanac></Almanac>
|
||||
</v-card>
|
||||
<v-card class="current-power">
|
||||
<CurrentPower></CurrentPower>
|
||||
</v-card>
|
||||
<v-card class="current-laundry-status">
|
||||
<CurrentLaundryStatus></CurrentLaundryStatus>
|
||||
</v-card>
|
||||
<v-card class="upstairs">
|
||||
<Indoor
|
||||
title="Upstairs"
|
||||
deviceName="main"></Indoor>
|
||||
</v-card>
|
||||
<v-card class="downstairs">
|
||||
<Indoor
|
||||
title="Downstairs"
|
||||
deviceName="basement"></Indoor>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
height: 100%;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 350px 210px 1fr;
|
||||
grid-template-rows: repeat(3, max-content);
|
||||
gap: 15px 15px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'current-weather almanac current-laundry-status'
|
||||
'current-weather almanac current-power'
|
||||
'upstairs downstairs .';
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 180px);
|
||||
grid-template-rows: repeat(2, max-content);
|
||||
gap: 15px 15px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'current-weather current-weather almanac almanac'
|
||||
'current-power current-laundry-status upstairs downstairs';
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
grid-template-rows: repeat(6, max-content);
|
||||
gap: 10px 0px;
|
||||
grid-template-areas:
|
||||
'current-weather'
|
||||
'almanac'
|
||||
'current-power'
|
||||
'current-laundry-status'
|
||||
'upstairs'
|
||||
'downstairs';
|
||||
}
|
||||
}
|
||||
|
||||
.current-weather {
|
||||
grid-area: current-weather;
|
||||
}
|
||||
|
||||
.almanac {
|
||||
grid-area: almanac;
|
||||
}
|
||||
|
||||
.current-power {
|
||||
grid-area: current-power;
|
||||
}
|
||||
|
||||
.current-laundry-status {
|
||||
grid-area: current-laundry-status;
|
||||
}
|
||||
|
||||
.upstairs {
|
||||
grid-area: upstairs;
|
||||
}
|
||||
|
||||
.downstairs {
|
||||
grid-area: downstairs;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
import { createIndoorStore } from '@/stores/indoorStore';
|
||||
import { ConvertCToF } from '@/temperatureConverter';
|
||||
import { ConvertMillibarToInchesOfMercury } from '@/pressureConverter';
|
||||
import ValueChart from '../components/ValueChart.vue';
|
||||
import ValueChart from '@/components/ValueChart.vue';
|
||||
import TimeRange from '@/components/TimeRange.vue';
|
||||
import TimeSpan from '@/models/time-span';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { subHours } from 'date-fns';
|
||||
import { useWeatherStore } from '@/stores/weatherStore';
|
||||
import { ConvertPascalToInchesOfMercury } from '@/pressureConverter';
|
||||
import ValueChart from '../components/ValueChart.vue';
|
||||
import ValueChart from '@/components/ValueChart.vue';
|
||||
import TimeRange from '@/components/TimeRange.vue';
|
||||
import TimeSpan from '@/models/time-span';
|
||||
import { ConvertDegreesToShortLabel, ConvertWindDirectionToDegrees } from '@/windConverter';
|
||||
@@ -3,7 +3,7 @@
|
||||
import { subHours } from 'date-fns';
|
||||
import { usePowerStore } from '@/stores/powerStore';
|
||||
import { useWeatherStore } from '@/stores/weatherStore';
|
||||
import ValueChart from '../components/ValueChart.vue';
|
||||
import ValueChart from '@/components/ValueChart.vue';
|
||||
import TimeRange from '@/components/TimeRange.vue';
|
||||
import WeatherValueType from '@/models/weather/weather-value-type';
|
||||
import TimeSpan from '@/models/time-span';
|
||||
409
WebDisplay/src/pages/kiosk.vue
Normal file
409
WebDisplay/src/pages/kiosk.vue
Normal file
@@ -0,0 +1,409 @@
|
||||
<script lang="ts" setup>
|
||||
import { capitalize, ref } from 'vue';
|
||||
import { useWeatherStore } from '@/stores/weatherStore';
|
||||
import { useLaundryStore } from '@/stores/laundryStore';
|
||||
import { usePowerStore } from '@/stores/powerStore';
|
||||
import { useHomeAssistantStore } from '@/stores/homeAssistantStore';
|
||||
import CalendarAgenda from '@/components/CalendarAgenda.vue';
|
||||
import LongPressButton from '@/components/LongPressButton.vue';
|
||||
import PressureTrendArrow from '@/components/PressureTrendArrow.vue';
|
||||
|
||||
const outOfDateDuration = 5000;
|
||||
|
||||
const showFeelsLike = ref(true);
|
||||
|
||||
const weatherStore = useWeatherStore();
|
||||
weatherStore.start();
|
||||
|
||||
const laundryStore = useLaundryStore();
|
||||
laundryStore.start();
|
||||
|
||||
const powerStore = usePowerStore();
|
||||
powerStore.start();
|
||||
|
||||
const homeAssistantStore = useHomeAssistantStore();
|
||||
homeAssistantStore.start();
|
||||
|
||||
const currentTime = ref(new Date());
|
||||
|
||||
const timeFormatter = new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
|
||||
|
||||
function alarmState(state: string): string {
|
||||
switch (state) {
|
||||
case 'armed_home':
|
||||
return 'Armed';
|
||||
case 'armed_away':
|
||||
return 'Armed';
|
||||
case 'disarmed':
|
||||
return 'Disarmed';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function getTemperature(): string {
|
||||
if (showFeelsLike.value && weatherStore.current?.WindChill) {
|
||||
return weatherStore.current?.WindChill?.toFixed(0) + '°';
|
||||
} else if (showFeelsLike.value && weatherStore.current?.HeatIndex) {
|
||||
return weatherStore.current?.HeatIndex?.toFixed(0) + '°';
|
||||
} else {
|
||||
return weatherStore.current?.Temperature?.toFixed(0) + '°';
|
||||
}
|
||||
}
|
||||
|
||||
function weatherOutOfDate(): boolean {
|
||||
return weatherStore.current?.Timestamp !== undefined && Date.now() - weatherStore.current.Timestamp.getTime() >= outOfDateDuration;
|
||||
}
|
||||
|
||||
function powerOutOfDate(): boolean {
|
||||
return powerStore.current?.Timestamp !== undefined && Date.now() - powerStore.current.Timestamp.getTime() >= outOfDateDuration;
|
||||
}
|
||||
|
||||
function getTemperatureClass(): string {
|
||||
if (weatherOutOfDate()) {
|
||||
return 'out-of-date-reading';
|
||||
} else if (showFeelsLike.value && weatherStore.current?.WindChill) {
|
||||
return 'temperature-wind-chill';
|
||||
} else if (showFeelsLike.value && weatherStore.current?.HeatIndex) {
|
||||
return 'temperature-heat-index';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function dewPointDescription(dewPoint: number | undefined): string {
|
||||
if (dewPoint === undefined) {
|
||||
return '';
|
||||
} else if (dewPoint < 55) {
|
||||
return 'Dry';
|
||||
} else if (dewPoint <= 60) {
|
||||
return 'Comfortable';
|
||||
} else if (dewPoint <= 65) {
|
||||
return 'Slightly Humid';
|
||||
} else if (dewPoint <= 70) {
|
||||
return 'Humid';
|
||||
} else if (dewPoint <= 75) {
|
||||
return 'Very Humid';
|
||||
} else if (dewPoint > 75) {
|
||||
return 'Oppressive';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
setInterval(() => (currentTime.value = new Date()), 1000);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container
|
||||
fluid
|
||||
class="kiosk-container">
|
||||
<div class="kiosk-sidebar">
|
||||
<div
|
||||
name="kiosk-time"
|
||||
class="kiosk-time text-center">
|
||||
{{ timeFormatter.format(currentTime) }}
|
||||
</div>
|
||||
<div class="kiosk-date text-center pb-4">
|
||||
{{ dateFormatter.format(currentTime) }}
|
||||
</div>
|
||||
<div
|
||||
class="kiosk-temperature text-center pb-3"
|
||||
:class="getTemperatureClass()"
|
||||
@click="showFeelsLike = !showFeelsLike"
|
||||
v-if="weatherStore.current">
|
||||
<div class="display-item">
|
||||
<span class="display-item-header">Temperature</span>
|
||||
<span class="display-item-value">{{ getTemperature() }}</span>
|
||||
<span class="display-item-label">{{ showFeelsLike ? 'Feels Like' : 'Actual' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="kiosk-humidity text-center pb-3"
|
||||
:class="{ 'out-of-date-reading': weatherOutOfDate() }"
|
||||
v-if="weatherStore.current">
|
||||
<div class="display-item">
|
||||
<span class="display-item-header">Dew Point</span>
|
||||
<span class="display-item-value">
|
||||
{{ weatherStore.current?.DewPoint?.toFixed(0) + '°' }}
|
||||
</span>
|
||||
<span class="display-item-label">{{ dewPointDescription(weatherStore.current?.DewPoint) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="kiosk-wind text-center pb-3"
|
||||
:class="{ 'out-of-date-reading': weatherOutOfDate() }"
|
||||
v-if="weatherStore.current">
|
||||
<div class="display-item">
|
||||
<span class="display-item-header">Wind</span>
|
||||
<span class="display-item-value">
|
||||
<span>
|
||||
{{ weatherStore.current?.WindSpeed?.toFixed(1) }}
|
||||
</span>
|
||||
<WindDirectionArrow :windDirection="weatherStore.current?.WindDirection" />
|
||||
</span>
|
||||
<span class="display-item-label">MPH</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="kiosk-pressure text-center pb-3"
|
||||
:class="{ 'out-of-date-reading': weatherOutOfDate() }"
|
||||
v-if="weatherStore.current">
|
||||
<div class="display-item">
|
||||
<span class="display-item-header">Pressure</span>
|
||||
<span class="display-item-value">
|
||||
<span>
|
||||
{{ (weatherStore.current?.Pressure! / 100).toFixed(0) }}
|
||||
</span>
|
||||
<PressureTrendArrow :pressureDifference="weatherStore.current?.PressureDifferenceThreeHour" />
|
||||
</span>
|
||||
<span class="display-item-label">hPa</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="kiosk-generation text-center pt-4"
|
||||
:class="{ 'out-of-date-reading': powerOutOfDate() }"
|
||||
v-if="powerStore.current">
|
||||
<v-icon
|
||||
class="kiosk-device-icon"
|
||||
icon="mdi-solar-power-variant" />
|
||||
<div class="kiosk-device-text">
|
||||
{{ (powerStore.current!.Generation < 0 ? 0 : powerStore.current!.Generation) + ' W' }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="kiosk-consumption text-center pt-4"
|
||||
:class="{ 'out-of-date-reading': powerOutOfDate() }"
|
||||
v-if="powerStore.current">
|
||||
<v-icon
|
||||
class="kiosk-device-icon"
|
||||
icon="mdi-home-lightning-bolt" />
|
||||
<div class="kiosk-device-text">
|
||||
{{ powerStore.current.Consumption + ' W' }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="kiosk-washer text-center pt-4"
|
||||
v-if="laundryStore?.current?.washer !== undefined"
|
||||
:class="laundryStore.current.washer ? 'warning' : 'normal'">
|
||||
<v-icon
|
||||
class="kiosk-device-icon"
|
||||
icon="mdi-washing-machine" />
|
||||
<div class="kiosk-device-text">
|
||||
{{ laundryStore.current.washer ? 'On' : 'Off' }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="kiosk-dryer text-center pt-4"
|
||||
v-if="laundryStore?.current?.dryer !== undefined"
|
||||
:class="laundryStore.current.dryer ? 'warning' : 'normal'">
|
||||
<v-icon
|
||||
class="kiosk-device-icon"
|
||||
icon="mdi-tumble-dryer" />
|
||||
<div class="kiosk-device-text">
|
||||
{{ laundryStore.current.dryer ? 'On' : 'Off' }}
|
||||
</div>
|
||||
</div>
|
||||
<LongPressButton
|
||||
class="kiosk-garage-door text-center pt-4"
|
||||
v-if="homeAssistantStore?.garageState"
|
||||
:duration="2000"
|
||||
:increment="100"
|
||||
:progress-size="38"
|
||||
v-on:longPress="homeAssistantStore.toggleGarage()"
|
||||
:class="homeAssistantStore.garageState === 'closed' ? 'normal' : 'warning'">
|
||||
<v-icon
|
||||
class="kiosk-device-icon"
|
||||
:icon="homeAssistantStore.garageState === 'closed' ? 'mdi-garage' : 'mdi-garage-open'" />
|
||||
<div class="kiosk-device-text">
|
||||
{{ capitalize(homeAssistantStore.garageState) }}
|
||||
</div>
|
||||
</LongPressButton>
|
||||
<div
|
||||
class="kiosk-house-alarm text-center pt-4"
|
||||
v-if="homeAssistantStore?.houseAlarmState"
|
||||
:class="homeAssistantStore.houseAlarmState === 'disarmed' ? 'normal' : 'warning'">
|
||||
<v-icon
|
||||
class="kiosk-device-icon"
|
||||
icon="mdi-shield-home" />
|
||||
<div class="kiosk-device-text">
|
||||
{{ alarmState(homeAssistantStore.houseAlarmState) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kiosk-content">
|
||||
<CalendarAgenda
|
||||
class="kiosk-calendar"
|
||||
days="10"
|
||||
:refresh-interval="5 * 60 * 1000" />
|
||||
<NationalDays class="kiosk-national-days" />
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.kiosk-container {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
background-color: #212428;
|
||||
color: #ebebeb;
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: 'kiosk-sidebar kiosk-content';
|
||||
}
|
||||
|
||||
.kiosk-row {
|
||||
padding-top: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kiosk-sidebar {
|
||||
padding: 5px;
|
||||
background-color: #121212;
|
||||
grid-area: kiosk-sidebar;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 50%);
|
||||
grid-template-rows: repeat(7, auto) 1fr;
|
||||
grid-column-gap: 0px;
|
||||
grid-row-gap: 0px;
|
||||
|
||||
grid-template-areas:
|
||||
'kiosk-time kiosk-time'
|
||||
'kiosk-date kiosk-date'
|
||||
'kiosk-temperature kiosk-humidity'
|
||||
'kiosk-wind kiosk-pressure'
|
||||
'kiosk-generation kiosk-consumption'
|
||||
'kiosk-washer kiosk-dryer'
|
||||
'kiosk-garage-door kiosk-house-alarm';
|
||||
}
|
||||
|
||||
.kiosk-content {
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 30px);
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(4, 25%);
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'kiosk-calendar kiosk-national-days kiosk-national-days'
|
||||
'kiosk-calendar kiosk-national-days kiosk-national-days'
|
||||
'kiosk-calendar kiosk-national-days kiosk-national-days'
|
||||
'kiosk-calendar kiosk-national-days kiosk-national-days';
|
||||
}
|
||||
|
||||
.kiosk-time {
|
||||
font-size: 2.8rem;
|
||||
grid-area: kiosk-time;
|
||||
}
|
||||
|
||||
.kiosk-date {
|
||||
font-size: 1.25rem;
|
||||
grid-area: kiosk-date;
|
||||
}
|
||||
|
||||
.kiosk-temperature {
|
||||
font-size: 2.9rem;
|
||||
grid-area: kiosk-temperature;
|
||||
}
|
||||
|
||||
.kiosk-humidity {
|
||||
font-size: 2.9rem;
|
||||
grid-area: kiosk-humidity;
|
||||
}
|
||||
|
||||
.kiosk-wind {
|
||||
font-size: 2rem;
|
||||
grid-area: kiosk-wind;
|
||||
}
|
||||
|
||||
.kiosk-pressure {
|
||||
font-size: 2rem;
|
||||
grid-area: kiosk-pressure;
|
||||
}
|
||||
|
||||
.kiosk-generation {
|
||||
grid-area: kiosk-generation;
|
||||
}
|
||||
|
||||
.kiosk-consumption {
|
||||
grid-area: kiosk-consumption;
|
||||
}
|
||||
|
||||
.kiosk-washer {
|
||||
grid-area: kiosk-washer;
|
||||
}
|
||||
|
||||
.kiosk-dryer {
|
||||
grid-area: kiosk-dryer;
|
||||
}
|
||||
|
||||
.kiosk-garage-door {
|
||||
grid-area: kiosk-garage-door;
|
||||
}
|
||||
|
||||
.kiosk-house-alarm {
|
||||
grid-area: kiosk-house-alarm;
|
||||
}
|
||||
|
||||
.kiosk-device-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.kiosk-device-text {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.kiosk-calendar {
|
||||
grid-area: kiosk-calendar;
|
||||
}
|
||||
|
||||
.kiosk-national-days {
|
||||
grid-area: kiosk-national-days;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #d09f27;
|
||||
}
|
||||
|
||||
.normal {
|
||||
color: #208b20;
|
||||
}
|
||||
|
||||
.display-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.display-item-value {
|
||||
line-height: calc(100% + 2px);
|
||||
}
|
||||
|
||||
.display-item-header {
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.display-item-label {
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.temperature-wind-chill {
|
||||
color: #4d4dff;
|
||||
}
|
||||
|
||||
.temperature-heat-index {
|
||||
color: #ff4d4d;
|
||||
}
|
||||
|
||||
.out-of-date-reading {
|
||||
color: #d09f27;
|
||||
}
|
||||
</style>
|
||||
23
WebDisplay/src/stores/calendarStore.ts
Normal file
23
WebDisplay/src/stores/calendarStore.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import axios from 'axios';
|
||||
import Environment from '@/environment';
|
||||
import CalendarEntry from '@/models/calendar/calendar-entry';
|
||||
import NationalDayEntry from '@/models/calendar/national-day';
|
||||
|
||||
export const useCalendarStore = defineStore('calendar', {
|
||||
state: () => {
|
||||
return {};
|
||||
},
|
||||
actions: {
|
||||
async getUpcoming(days: number, includeHolidays: boolean): Promise<CalendarEntry[]> {
|
||||
const response = await axios.get<CalendarEntry[]>(Environment.getUrlPrefix() + `:8081/api/calendar/calendar/upcoming?days=${days}&includeHolidays=${includeHolidays}`);
|
||||
|
||||
return response.data;
|
||||
},
|
||||
async getNationalDays(): Promise<NationalDayEntry[]> {
|
||||
const response = await axios.get<NationalDayEntry[]>(Environment.getUrlPrefix() + `:8081/api/calendar/national-days/today?timezone=${Intl.DateTimeFormat().resolvedOptions().timeZone}`);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
});
|
||||
74
WebDisplay/src/stores/homeAssistantStore.ts
Normal file
74
WebDisplay/src/stores/homeAssistantStore.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { createConnection, subscribeEntities, createLongLivedTokenAuth, Connection, callService } from 'home-assistant-js-websocket';
|
||||
import Environment from '@/environment';
|
||||
|
||||
export const useHomeAssistantStore = defineStore('home-assistant', {
|
||||
state: () => {
|
||||
return {
|
||||
garageState: null as string | null,
|
||||
houseAlarmState: null as string | null,
|
||||
_connection: null as Connection | null
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
async start() {
|
||||
if (!Environment.getHomeAssistantUrl() || !Environment.getHomeAssistantToken()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const garageDevice = Environment.getGarageDevice();
|
||||
const alarmDevice = Environment.getAlarmDevice();
|
||||
|
||||
const auth = createLongLivedTokenAuth(Environment.getHomeAssistantUrl(), Environment.getHomeAssistantToken());
|
||||
|
||||
this._connection = await createConnection({ auth });
|
||||
|
||||
subscribeEntities(this._connection as Connection, (entities) => {
|
||||
const garageEntity = entities[garageDevice];
|
||||
|
||||
if (garageEntity) {
|
||||
this.$patch({ garageState: garageEntity.state });
|
||||
}
|
||||
|
||||
const houseAlarmEntity = entities[alarmDevice];
|
||||
|
||||
if (houseAlarmEntity) {
|
||||
this.$patch({ houseAlarmState: houseAlarmEntity.state });
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(async () => await this._connection?.ping(), 5000);
|
||||
},
|
||||
async stop() {
|
||||
this._connection?.close();
|
||||
this._connection = null;
|
||||
},
|
||||
async toggleGarage() {
|
||||
const garageDevice = Environment.getGarageDevice();
|
||||
|
||||
let action: string | null;
|
||||
|
||||
switch (this.garageState) {
|
||||
case 'closed':
|
||||
action = 'open_cover';
|
||||
break;
|
||||
case 'open':
|
||||
action = 'close_cover';
|
||||
break;
|
||||
case 'closing':
|
||||
case 'opening':
|
||||
action = 'stop_cover';
|
||||
break;
|
||||
default:
|
||||
action = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
callService(this._connection as Connection, 'cover', action, { entity_id: garageDevice });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
||||
import { dateReviver } from '@/dateReviver';
|
||||
import axios from 'axios';
|
||||
import Environment from '@/environment';
|
||||
import PowerStatus from '@/models/power/power-status';
|
||||
@@ -27,7 +28,7 @@ export const usePowerStore = defineStore('power', {
|
||||
await this._connection.start();
|
||||
|
||||
this._connection.on('LatestSample', (message: string) => {
|
||||
this.$patch({ current: JSON.parse(message) });
|
||||
this.$patch({ current: JSON.parse(message, dateReviver) });
|
||||
});
|
||||
},
|
||||
async stop() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
||||
import { dateReviver } from '@/dateReviver';
|
||||
import axios from 'axios';
|
||||
import Environment from '@/environment';
|
||||
import WeatherUpdate from '@/models/weather/weather-update';
|
||||
@@ -32,7 +33,9 @@ export const useWeatherStore = defineStore('weather', {
|
||||
await this._connection.start();
|
||||
|
||||
this._connection.on('LatestReading', (message: string) => {
|
||||
this.$patch({ current: JSON.parse(message) });
|
||||
const json: WeatherUpdate = JSON.parse(message, dateReviver);
|
||||
|
||||
this.$patch({ current: json });
|
||||
});
|
||||
},
|
||||
async stop() {
|
||||
|
||||
10
WebDisplay/typed-router.d.ts
vendored
10
WebDisplay/typed-router.d.ts
vendored
@@ -40,10 +40,12 @@ import type {
|
||||
declare module 'vue-router/auto/routes' {
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||
'/indoor': RouteRecordInfo<'/indoor', '/indoor', 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>>,
|
||||
'/summary': RouteRecordInfo<'/summary', '/summary', Record<never, never>, Record<never, never>>,
|
||||
'//': RouteRecordInfo<'//', '/', Record<never, never>, Record<never, never>>,
|
||||
'//indoor': RouteRecordInfo<'//indoor', '/indoor', 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>>,
|
||||
'//summary': RouteRecordInfo<'//summary', '/summary', Record<never, never>, Record<never, never>>,
|
||||
'/kiosk': RouteRecordInfo<'/kiosk', '/kiosk', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ export default defineConfig({
|
||||
plugins: [
|
||||
VueRouter(),
|
||||
Vue({
|
||||
template: { transformAssetUrls },
|
||||
template: { transformAssetUrls }
|
||||
}),
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
||||
Vuetify({
|
||||
autoImport: true,
|
||||
styles: {
|
||||
configFile: 'src/styles/settings.scss',
|
||||
},
|
||||
configFile: 'src/styles/settings.scss'
|
||||
}
|
||||
}),
|
||||
Components(),
|
||||
ViteFonts({
|
||||
@@ -29,20 +29,27 @@ export default defineConfig({
|
||||
families: [
|
||||
{
|
||||
name: 'Roboto',
|
||||
styles: 'wght@100;300;400;500;700;900',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
styles: 'wght@100;300;400;500;700;900'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
],
|
||||
define: { 'process.env': {} },
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
|
||||
extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue']
|
||||
},
|
||||
server: {
|
||||
port: 4200,
|
||||
port: 4200
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
sass: {
|
||||
api: 'modern-compiler'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user