Repository: isaacrlevin/PresenceLight Branch: main Commit: 5603889a898e Files: 230 Total size: 696.5 KB Directory structure: gitextract_bd12mvz1/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ ├── Azure_Blob_Deploy.yml │ ├── Choco.yml │ ├── Deploy_Desktop.yml │ ├── Deploy_Web.yml │ ├── Sign.yml │ └── WinGet.yml ├── .gitignore ├── Build/ │ ├── Signing/ │ │ └── filelist.txt │ ├── Worker/ │ │ ├── presencelight.crt │ │ ├── presencelight.service │ │ └── trust-cert.sh │ └── scripts/ │ ├── push-choco.ps1 │ ├── push-winget.ps1 │ ├── update-desktop-settings.ps1 │ └── update-web-settings.ps1 ├── LICENSE ├── PrivacyPolicy.md ├── README.md ├── chocolatey/ │ ├── PresenceLight.nuspec │ └── tools/ │ ├── ChocolateyBeforeModify.ps1 │ ├── ChocolateyInstall.ps1 │ ├── ChocolateyUninstall.ps1 │ ├── LICENSE.txt │ └── Verification.txt ├── docker-compose-example.yml ├── docs/ │ ├── CONTRIBUTING.md │ ├── configure-custom-api.md │ ├── configure-entra-app.md │ ├── configure-hardware.md │ ├── desktop-README.md │ ├── faq.md │ └── web-README.md ├── src/ │ ├── .editorconfig │ ├── DesktopClient/ │ │ ├── Directory.Build.props │ │ ├── Directory.Build.targets │ │ ├── PresenceLight/ │ │ │ ├── App.xaml │ │ │ ├── App.xaml.cs │ │ │ ├── MainWindow.xaml │ │ │ ├── MainWindow.xaml.cs │ │ │ ├── PresenceLight.csproj │ │ │ ├── PresenceLight.exe.gui │ │ │ ├── Properties/ │ │ │ │ ├── AssemblyInfo.cs │ │ │ │ ├── PublishProfiles/ │ │ │ │ │ ├── WinARM64.pubxml │ │ │ │ │ ├── WinX64.pubxml │ │ │ │ │ └── WinX86.pubxml │ │ │ │ ├── Resources.Designer.cs │ │ │ │ ├── Resources.resx │ │ │ │ ├── Settings.Designer.cs │ │ │ │ ├── Settings.settings │ │ │ │ └── app.manifest │ │ │ ├── Services/ │ │ │ │ ├── Constants.cs │ │ │ │ ├── MessageBoxHelper.cs │ │ │ │ ├── NotifyIcon.cs │ │ │ │ ├── Settings/ │ │ │ │ │ ├── AppPackageSettingsService.cs │ │ │ │ │ └── StandaloneSettingsService.cs │ │ │ │ ├── SingleInstanceAppMutex.cs │ │ │ │ └── Telemetry/ │ │ │ │ └── DiagnosticsClient.cs │ │ │ ├── appsettings.json │ │ │ └── wwwroot/ │ │ │ └── index.html │ │ └── PresenceLight.Package/ │ │ ├── Package-Local.appxmanifest │ │ ├── Package-Nightly.appxmanifest │ │ ├── Package.appinstaller │ │ ├── Package.appxmanifest │ │ ├── Package.xml │ │ └── PresenceLight.Package.wapproj │ ├── DockerCompose/ │ │ ├── .dockerignore │ │ ├── docker-compose.dcproj │ │ ├── docker-compose.override.yml │ │ └── docker-compose.yml │ ├── PresenceLight.Core/ │ │ ├── Configuration/ │ │ │ ├── AAD.cs │ │ │ ├── AppState.cs │ │ │ ├── AvailabilityStatus.cs │ │ │ ├── Base.cs │ │ │ ├── BaseLight.cs │ │ │ ├── CustomApi.cs │ │ │ ├── CustomApiSetting.cs │ │ │ ├── Hue.cs │ │ │ ├── ISettingsService.cs │ │ │ ├── LIFX.cs │ │ │ ├── LightSettings.cs │ │ │ ├── LocalSerialHost.cs │ │ │ ├── LocalSerialHostSetting.cs │ │ │ ├── Statuses.cs │ │ │ ├── Wiz.cs │ │ │ └── Yeelight.cs │ │ ├── GraphServices/ │ │ │ ├── AuthorizationProvider.cs │ │ │ ├── GetIsInitialized/ │ │ │ │ ├── GetIsInitializedCommand.cs │ │ │ │ └── GetIsInitializedHandler.cs │ │ │ ├── GetPhoto/ │ │ │ │ ├── GetPhotoCommand.cs │ │ │ │ └── GetPhotoHandler.cs │ │ │ ├── GetPresence/ │ │ │ │ ├── GetPresenceCommand.cs │ │ │ │ └── GetPresenceHandler.cs │ │ │ ├── GetProfile/ │ │ │ │ ├── GetProfileCommand.cs │ │ │ │ └── GetProfileHandler.cs │ │ │ ├── GetProfileAndPresence/ │ │ │ │ ├── GetProfileAndPresenceCommand.cs │ │ │ │ └── GetProfileAndPresenceHandler.cs │ │ │ ├── GraphWrapper.cs │ │ │ ├── Initialize/ │ │ │ │ ├── InitializeCommand.cs │ │ │ │ └── InitializeHandler.cs │ │ │ ├── LoginService.cs │ │ │ └── TokenCacheHelper.cs │ │ ├── Helpers.cs │ │ ├── Lights/ │ │ │ ├── CustomApiService/ │ │ │ │ ├── CustomApiService.cs │ │ │ │ ├── Initialize/ │ │ │ │ │ ├── InitializeCommand.cs │ │ │ │ │ └── InitializeHandler.cs │ │ │ │ └── SetColor/ │ │ │ │ ├── SetColorCommand.cs │ │ │ │ └── SetColorHandler.cs │ │ │ ├── HueServices/ │ │ │ │ ├── FindBridge/ │ │ │ │ │ ├── FindBridgeCommand.cs │ │ │ │ │ └── FindBridgeHandler.cs │ │ │ │ ├── GetGroups/ │ │ │ │ │ ├── GetGroupsCommand.cs │ │ │ │ │ └── GetGroupsHandler.cs │ │ │ │ ├── GetLights/ │ │ │ │ │ ├── GetLightsCommand.cs │ │ │ │ │ └── GetLightsHandler.cs │ │ │ │ ├── HueService.cs │ │ │ │ ├── Initialize/ │ │ │ │ │ ├── InitializeCommand.cs │ │ │ │ │ └── InitializeHandler.cs │ │ │ │ ├── RegisterBridge/ │ │ │ │ │ ├── RegisterBridgeCommand.cs │ │ │ │ │ └── RegisterBridgeHandler.cs │ │ │ │ └── SetColor/ │ │ │ │ ├── SetColorCommand.cs │ │ │ │ └── SetColorHandler.cs │ │ │ ├── LifxServices/ │ │ │ │ ├── GetAllGroups/ │ │ │ │ │ ├── GetAllGroupsCommand.cs │ │ │ │ │ └── GetAllGroupsHandler.cs │ │ │ │ ├── GetAllLights/ │ │ │ │ │ ├── GetAllLightsCommand.cs │ │ │ │ │ └── GetAllLightsHandler.cs │ │ │ │ ├── Initialize/ │ │ │ │ │ ├── InitializeCommand.cs │ │ │ │ │ └── InitializeHandler.cs │ │ │ │ ├── LIFXOAuthHelper.cs │ │ │ │ ├── LifxService.cs │ │ │ │ └── SetColor/ │ │ │ │ ├── SetColorCommand.cs │ │ │ │ └── SetColorHandler.cs │ │ │ ├── LocalSerialHostService/ │ │ │ │ ├── GetSerialHosts/ │ │ │ │ │ ├── GetSerialHostsCommand.cs │ │ │ │ │ └── GetSerialHostsHandler.cs │ │ │ │ ├── Initialize/ │ │ │ │ │ ├── InitializeCommand.cs │ │ │ │ │ └── InitializeHandler.cs │ │ │ │ ├── LocalSerialHost.cs │ │ │ │ └── SetColor/ │ │ │ │ ├── SetColorCommand.cs │ │ │ │ └── SetColorHandler.cs │ │ │ ├── RemoteHueServices/ │ │ │ │ ├── GetGroups/ │ │ │ │ │ ├── GetGroupsCommand.cs │ │ │ │ │ └── GetGroupsHandler.cs │ │ │ │ ├── GetLights/ │ │ │ │ │ ├── GetLightsCommand.cs │ │ │ │ │ └── GetLightsHandler.cs │ │ │ │ ├── RegisterBridge/ │ │ │ │ │ ├── RegisterBridgeCommand.cs │ │ │ │ │ └── RegisterBridgeHandler.cs │ │ │ │ ├── RemoteAuthenticationClient.cs │ │ │ │ ├── RemoteHueService.cs │ │ │ │ └── SetColor/ │ │ │ │ ├── SetColorCommand.cs │ │ │ │ └── SetColorHandler.cs │ │ │ ├── ServicesExtensions.cs │ │ │ ├── WizServices/ │ │ │ │ ├── GetLights/ │ │ │ │ │ ├── GetLightCommand.cs │ │ │ │ │ └── GetLightHandler.cs │ │ │ │ ├── IPAddressExtensions.cs │ │ │ │ ├── SetColor/ │ │ │ │ │ ├── SetColorCommand.cs │ │ │ │ │ └── SetColorHandler.cs │ │ │ │ ├── WizLight.cs │ │ │ │ └── WizService.cs │ │ │ ├── WorkingHoursServices/ │ │ │ │ ├── IsInWorkingHours/ │ │ │ │ │ ├── IsInWorkingHoursCommand.cs │ │ │ │ │ └── IsInWorkingHoursHandler.cs │ │ │ │ ├── UseWorkingHours/ │ │ │ │ │ ├── UseWorkingHoursCommand.cs │ │ │ │ │ └── UseWorkingHoursHandler.cs │ │ │ │ └── WorkingHoursService.cs │ │ │ └── YeelightServices/ │ │ │ ├── FindLights/ │ │ │ │ ├── FindLightsCommand.cs │ │ │ │ └── FindLightsHandler.cs │ │ │ ├── SetColor/ │ │ │ │ ├── SetColorCommand.cs │ │ │ │ └── SetColorHandler.cs │ │ │ └── YeelightService.cs │ │ ├── Logging/ │ │ │ ├── ILoggerExtensions.cs │ │ │ └── PresenceEventsLogSink.cs │ │ └── PresenceLight.Core.csproj │ ├── PresenceLight.Razor/ │ │ ├── Components/ │ │ │ ├── Layout/ │ │ │ │ ├── MainLayout.razor │ │ │ │ ├── MainLayout.razor.css │ │ │ │ ├── NavMenu.razor │ │ │ │ └── NavMenu.razor.css │ │ │ ├── Pages/ │ │ │ │ ├── About.razor │ │ │ │ ├── Color.razor │ │ │ │ ├── Color.razor.css │ │ │ │ ├── CustomApiSetup.razor │ │ │ │ ├── HueSetup.razor │ │ │ │ ├── Index.razor │ │ │ │ ├── Index.razor.css │ │ │ │ ├── Lifx.razor │ │ │ │ ├── LocalSerialHostSetup.razor │ │ │ │ ├── Logs.razor │ │ │ │ ├── Settings.razor │ │ │ │ ├── Wiz.razor │ │ │ │ └── Yeelight.razor │ │ │ ├── PresenceLightClientApp.razor │ │ │ ├── Shared/ │ │ │ │ ├── Confirm.razor │ │ │ │ ├── LoginDisplay.razor │ │ │ │ └── Statuses.razor │ │ │ └── _Imports.razor │ │ ├── PresenceLight.Razor.csproj │ │ ├── Services/ │ │ │ ├── AppInfo.cs │ │ │ ├── AppVersionTelemetryInitializer.cs │ │ │ └── WebAppSettingsService.cs │ │ └── wwwroot/ │ │ ├── css/ │ │ │ ├── open-iconic/ │ │ │ │ ├── FONT-LICENSE │ │ │ │ ├── ICON-LICENSE │ │ │ │ ├── README.md │ │ │ │ └── font/ │ │ │ │ └── fonts/ │ │ │ │ └── open-iconic.otf │ │ │ └── site.css │ │ └── js/ │ │ └── site.js │ ├── PresenceLight.Web/ │ │ ├── .config/ │ │ │ └── dotnet-tools.json │ │ ├── App.razor │ │ ├── AppOld.razor │ │ ├── Dockerfile │ │ ├── Dockerfile.debian-arm32 │ │ ├── Dockerfile.debian-arm64 │ │ ├── Pages/ │ │ │ ├── Error.cshtml │ │ │ ├── Error.cshtml.cs │ │ │ └── _Host.cshtml │ │ ├── PresenceLight.Web.csproj │ │ ├── PresenceLightSettings.json │ │ ├── Program.cs │ │ ├── Program_New.cs │ │ ├── Program_Old.cs │ │ ├── Properties/ │ │ │ ├── PublishProfiles/ │ │ │ │ └── FolderProfile.pubxml │ │ │ ├── launchSettings.json │ │ │ ├── serviceDependencies.json │ │ │ └── serviceDependencies.local.json │ │ ├── Routes.razor │ │ ├── ServiceCollectionExtensions.cs │ │ ├── Worker.cs │ │ ├── _Imports.razor │ │ └── appsettings.json │ └── PresenceLight.sln └── version.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/FUNDING.yml ================================================ github: isaacrlevin ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. ## Further details - OS: [e.g. iOS] - PresenceLight App Type (Desktop, Blazor) - PresenceLight Build (Nightly, Store, etc) - PresenceLight Version (The # in About) - Did you clear settings.json? (C:\Users\username\AppData\Local\Packages\37828IsaacLevin.197278F15330A.SomeValue\LocalState\settings.json - ASP.NET Core version ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: nuget directory: "/src" schedule: interval: daily time: "10:00" open-pull-requests-limit: 10 ignore: - dependency-name: Blazorise.Bootstrap versions: - 0.9.3 - 0.9.3.1 - 0.9.3.3 - 0.9.3.4 - 0.9.3.5 - dependency-name: Microsoft.Identity.Client versions: - 4.26.0 - 4.27.0 - 4.28.0 - 4.28.1 - 4.29.0 - 4.30.0 - dependency-name: Blazorise.Icons.FontAwesome versions: - 0.9.3 - 0.9.3.1 - 0.9.3.3 - 0.9.3.4 - 0.9.3.5 - dependency-name: Microsoft.Identity.Web versions: - 1.6.0 - 1.7.0 - 1.8.1 - 1.8.2 - 1.9.0 - dependency-name: Microsoft.Identity.Web.MicrosoftGraphBeta versions: - 1.6.0 - 1.7.0 - 1.8.1 - 1.8.2 - 1.9.0 - dependency-name: Microsoft.Identity.Web.UI versions: - 1.6.0 - 1.7.0 - 1.8.1 - 1.8.2 - 1.9.0 - dependency-name: Nerdbank.GitVersioning versions: - 3.4.190 - dependency-name: Newtonsoft.Json versions: - 13.0.1 - dependency-name: YeelightAPI versions: - 1.10.1 - 1.10.2 - dependency-name: Microsoft.AspNetCore.Authentication.OpenIdConnect versions: - 5.0.3 - 5.0.4 - dependency-name: Serilog.Extensions.Hosting versions: - 4.0.0 - 4.1.0 - 4.1.2 - dependency-name: Microsoft.ApplicationInsights.NLogTarget versions: - 2.17.0 - dependency-name: Microsoft.ApplicationInsights.AspNetCore versions: - 2.17.0 - dependency-name: Microsoft.ApplicationInsights.WorkerService versions: - 2.17.0 - dependency-name: IdentityModel versions: - 5.0.1 ================================================ FILE: .github/workflows/Azure_Blob_Deploy.yml ================================================ name: Deploy Worker Apps to Azure Blob Storage on: workflow_dispatch: inputs: target: description: 'Target of Channel Name to Sign' required: true default: '' type: string workflow_call: inputs: target: description: 'Target of Channel Name to Sign' required: true default: '' type: string jobs: Deploy_Worker_Artifacts: name: Deploy Worker Artifacts environment: name: Deploy_Azure_Blob permissions: id-token: write # Required for requesting the JWT runs-on: ubuntu-latest steps: - name: Azure login uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Download ${{ inputs.target }} Artifacts uses: actions/download-artifact@v4 with: name: ${{ inputs.target }}_Signed path: ./Signed/${{ inputs.target }} - name: Archive Previous Release uses: azure/cli@v2 with: inlineScript: | account_name="${{ secrets.ACCOUNT_NAME }}" account_key="${{ secrets.ACCOUNT_KEY }}" source_container="${{ secrets.WORKER_CONTAINER }}" destination_container="${{ secrets.WORKER_ARCHIVE_CONTAINER }}" pattern="${{ inputs.target }}" # Get a list of blobs in the source container that match the pattern blobs=$(az storage blob list --account-name $account_name --account-key $account_key --container-name $source_container --query "[?contains(name, '$pattern')].name" -o tsv) # Loop through the blobs and copy each one to the destination container for blob in $blobs do echo "Copying blob $blob from $source_container to $destination_container" blob=$(echo $blob | tr -d '\r') az storage blob copy start \ --account-name $account_name \ --account-key $account_key \ --destination-container $destination_container \ --destination-blob $blob \ --source-account-name $account_name \ --source-account-key $account_key \ --source-container $source_container \ --source-blob $blob echo "Deleting blob $blob from $source_container" az storage blob delete \ --account-key $account_key \ --account-name $account_name \ --container-name $source_container \ --name $blob done azcliversion: latest - name: Upload Release to Blob Storage uses: azure/cli@v2 with: inlineScript: | az storage blob upload-batch --connection-string "${{ secrets.AZUREBLOBCONNECTIONSTRING }}" \ -d "${{ secrets.WORKER_CONTAINER }}" -s "./Signed" azcliversion: latest ================================================ FILE: .github/workflows/Choco.yml ================================================ on: workflow_call: workflow_dispatch: inputs: version: description: 'Version to deploy' required: true jobs: Deploy_Choco: name: Publish App to Chocolatey runs-on: windows-latest steps: - uses: actions/download-artifact@v4 name: Download Standalone Signed App Artifacts with: name: Standalone_Signed path: .\StandaloneSigned - uses: actions/download-artifact@v4 if: ${{ github.event_name == 'push' }} name: Download Build Artifacts with: name: BuildArtifacts path: .\BuildArtifacts - name: Set VERSION Environment Variable (PUSH) if: ${{ github.event_name == 'push' }} run: | $version = Get-Content ".\BuildArtifacts\version.txt" echo "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append shell: powershell - name: Set VERSION Environment Variable (Workflow Dispatch) if: ${{ github.event_name == 'workflow_dispatch' }} run: | version = "${{ inputs.Version }}" echo "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append shell: powershell - uses: actions/download-artifact@v4 name: Download Chocolatey Artifacts with: name: Chocolatey path: .\Chocolatey - name: Push to Chocolatey run: | .\BuildArtifacts\scripts\push-choco.ps1 -Version "${{ env.VERSION}}" -CHOCOAPIKEY "${{ secrets.CHOCOAPIKEY}}" shell: powershell - name: Setup tmate session if: ${{ failure() }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 15 ================================================ FILE: .github/workflows/Deploy_Desktop.yml ================================================ on: push: branches: [ main ] paths-ignore: - '.github/workflows/Deploy_Web.yml' - 'src/PresenceLight.Web/**' - 'src/DockerFiles/**' - '*..md' - 'docs/*..md' - 'Build/**' - 'chocolatey/**' pull_request: branches: [ main ] paths-ignore: - '.github/workflows/Deploy_Web.yml' - 'src/PresenceLight.Web/**' - 'src/DockerFiles/**' - '*..md' - 'docs/*..md' - 'Build/**' - 'chocolatey/**' jobs: Setup_Desktop: name: Setup App for Build runs-on: ubuntu-latest strategy: matrix: ChannelName: - Release - Nightly - Standalone env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true BuildConfiguration: Release ACTIONS_ALLOW_UNSECURE_COMMANDS: true Win10RID: net10.0-windows10.0.19041 steps: - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Use .NET Core SDK 8.0.x and 10.0.x uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x 10.0.x - name: Get Version from Nerdbank.GitVersioning uses: dotnet/nbgv@v0.4.2 with: setCommonVars: true - run: echo "BuildNumber - ${{ env.GitBuildVersionSimple }}" - name: Add Secrets to appsettings.json run: | ./Build/scripts/update-desktop-settings.ps1 -Release "${{ matrix.ChannelName}}" -Version "${{ env.GitBuildVersionSimple }}" -ApplicationId "${{ secrets.ApplicationId }}" ` -ClientSecret "${{ secrets.ClientSecret }}" -InstrumentationKey "${{ secrets.InstrumentationKey }}" ` -LIFXClientId "${{ secrets.LIFXClientId }}" -LIFXClientSecret "${{ secrets.LIFXClientSecret }}" ` -RemoteHueClientId "${{ secrets.RemoteHueClientId }}" -RemoteHueClientSecret "${{ secrets.RemoteHueClientSecret }}" ` -RemoteHueClientAppName "${{ secrets.RemoteHueClientAppName }}" shell: pwsh if: ${{ success() && github.event_name != 'pull_request' }} - name: Create Version File to Artifact run : | New-Item -Path ./Build -Name "version.txt" -ItemType "file" -Value "${{ env.GitBuildVersionSimple }}" shell: pwsh - name: Publish ${{ matrix.ChannelName }} Arifacts uses: actions/upload-artifact@v4 with: path: ./src name: PreBuild-${{ matrix.ChannelName }} - name: Publish Build Artifacts uses: actions/upload-artifact@v4 with: path: Build name: BuildArtifacts if: ${{ success() && matrix.ChannelName == 'Standalone' }} - name: Publish Chocolatey Artifacts uses: actions/upload-artifact@v4 with: path: chocolatey name: Chocolatey if: ${{ success() && matrix.ChannelName == 'Standalone' }} Build_WPF: name: Build App needs: Setup_Desktop runs-on: windows-latest strategy: matrix: ChannelName: [ Release, Nightly, Standalone ] env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true BuildConfiguration: Release ACTIONS_ALLOW_UNSECURE_COMMANDS: true Win10RID: net10.0-windows10.0.19041 steps: - name: setup-msbuild uses: microsoft/setup-msbuild@v1 - name: Use .NET Core SDK 8.0.x and 10.0.x uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x 10.0.x - name: Download PreBuild uses: actions/download-artifact@v4 with: name: PreBuild-${{ matrix.ChannelName }} path: ./src - name: Download Build Artifacts uses: actions/download-artifact@v4 with: name: BuildArtifacts path: ./BuildArtifacts - name: Set GitBuildVersionSimple Environment Variable run: | $version = Get-Content ".\BuildArtifacts\version.txt" echo "GitBuildVersionSimple=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append shell: pwsh - name: Create Directory for Channel run: mkdir ./${{ matrix.ChannelName }} - name: Update Badge Versions run: | # Update badges [xml]$badge = Get-Content ".\BuildArtifacts\ci_badge.svg" $badge.svg.g[1].text[2].InnerText = "${{ env.GitBuildVersionSimple }}" $badge.svg.g[1].text[3].InnerText = "${{ env.GitBuildVersionSimple }}" $badge.Save(".\${{ matrix.ChannelName }}\ci_badge.svg") [xml]$badge = Get-Content ".\BuildArtifacts\store_badge.svg" $badge.svg.g[1].text[2].InnerText = "${{ env.GitBuildVersionSimple }}" $badge.svg.g[1].text[3].InnerText = "${{ env.GitBuildVersionSimple }}" $badge.Save(".\${{ matrix.ChannelName }}\stable_badge.svg") shell: powershell - name: Setup Windows SDK uses: GuillaumeFalourd/setup-windows10-sdk-action@v2 with: sdk-version: 19041 if: ${{ success() && matrix.ChannelName != 'Standalone' }} - name: Build Standalone Presence Light x86 run: | dotnet restore .\src\DesktopClient\PresenceLight\PresenceLight.csproj dotnet publish .\src\DesktopClient\PresenceLight\PresenceLight.csproj -c ${{ env.BuildConfiguration }} /p:Version=${{ env.GitBuildVersionSimple }} /p:PublishProfile=Properties/PublishProfiles/WinX86.pubxml --property WarningLevel=3 if: ${{ success() && matrix.ChannelName == 'Standalone' }} - name: Build Standalone Presence Light x64 run: | dotnet restore .\src\DesktopClient\PresenceLight\PresenceLight.csproj dotnet publish .\src\DesktopClient\PresenceLight\PresenceLight.csproj -c ${{ env.BuildConfiguration }} /p:Version=${{ env.GitBuildVersionSimple }} /p:PublishProfile=Properties/PublishProfiles/WinX64.pubxml --property WarningLevel=3 if: ${{ success() && matrix.ChannelName == 'Standalone' }} - name: Build Standalone Presence Light ARM64 run: | dotnet restore .\src\DesktopClient\PresenceLight\PresenceLight.csproj dotnet publish .\src\DesktopClient\PresenceLight\PresenceLight.csproj -c ${{ env.BuildConfiguration }} /p:Version=${{ env.GitBuildVersionSimple }} /p:PublishProfile=Properties/PublishProfiles/WinARM64.pubxml --property WarningLevel=3 if: ${{ success() && matrix.ChannelName == 'Standalone' }} - name: Zip Standalone PresenceLight x86 Files run: | Compress-Archive -Path '.\src\DesktopClient\PresenceLight\bin\${{ env.BuildConfiguration }}\${{ env.Win10RID }}\win-x86\publish\*' ` -DestinationPath ".\${{ matrix.ChannelName }}\PresenceLight.${{ env.GitBuildVersionSimple }}-x86.zip" shell: powershell if: ${{ success() && matrix.ChannelName == 'Standalone' }} - name: Zip Standalone PresenceLight x64 Files run: | Compress-Archive -Path '.\src\DesktopClient\PresenceLight\bin\${{ env.BuildConfiguration }}\${{ env.Win10RID }}\win-x64\publish\*' ` -DestinationPath ".\${{ matrix.ChannelName }}\PresenceLight.${{ env.GitBuildVersionSimple }}-x64.zip" shell: powershell if: ${{ success() && matrix.ChannelName == 'Standalone' }} - name: Zip Standalone PresenceLight ARM Files run: | Compress-Archive -Path '.\src\DesktopClient\PresenceLight\bin\${{ env.BuildConfiguration }}\${{ env.Win10RID }}\win-arm64\publish\*' ` -DestinationPath ".\${{ matrix.ChannelName }}\PresenceLight.${{ env.GitBuildVersionSimple }}-win-arm64.zip" shell: powershell if: ${{ success() && matrix.ChannelName == 'Standalone' }} - name: Build Appx Package run: | msbuild '.\src\DesktopClient\PresenceLight.Package\PresenceLight.Package.wapproj' /p:VersionNumber=${{ env.GitBuildVersionSimple }} ` /p:ChannelName=${{ matrix.ChannelName }} /p:configuration='${{ env.BuildConfiguration }}' /p:IncludeSymbols=true /p:WarningLevel=3 ` /p:AppxPackageDir="${{ github.workspace }}\${{ matrix.ChannelName }}\" if: ${{ success() && matrix.ChannelName != 'Standalone' }} - name: Publish ${{ matrix.ChannelName }} Arifacts uses: actions/upload-artifact@v4 with: path: .\${{ matrix.ChannelName }} name: ${{ matrix.ChannelName }} - name: Setup Tmate session if: ${{ failure() }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 15 Code_Signing: name: Code Sign needs: Build_WPF strategy: matrix: target: [ Release, Nightly, Standalone ] uses: isaacrlevin/presencelight/.github/workflows/Sign.yml@main with: target: ${{ matrix.target }} secrets: inherit Deploy_Azure_Blob: name: Deploy Nightly App to Azure Blob Storage needs: Code_Signing if: ${{ github.event_name != 'pull_request' }} environment: name: Deploy_Azure_Blob url: ${{ steps.deploy_staging.outputs.webapp-url }} permissions: id-token: write # Required for requesting the JWT runs-on: ubuntu-latest steps: - name: Azure Login uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Download Nightly Signed uses: actions/download-artifact@v4 with: name: Nightly_Signed path: "./NightlySigned" - name: Upload Nightly App to Azure Blob Storage run: | Copy-Item "./NightlySigned" -Destination "./Upload" -Recurse -Verbose dir .\Upload\ az storage blob upload --account-key ${{ secrets.ACCOUNT_KEY }} --account-name ${{ secrets.ACCOUNT_NAME }} -f ./Upload/ci_badge.svg -n ci_badge.svg -c nightly --content-type image/svg+xml --debug --overwrite az storage blob upload --account-key ${{ secrets.ACCOUNT_KEY }} --account-name ${{ secrets.ACCOUNT_NAME }} -f ./Upload/PresenceLight.Package.appinstaller -n PresenceLight.Package.appinstaller -c nightly --content-type application/xml --debug --overwrite az storage blob upload-batch --account-key ${{ secrets.ACCOUNT_KEY }} --account-name ${{ secrets.ACCOUNT_NAME }} --source ./Upload --pattern *.appxbundle -d nightly --content-type application/vns.ms-appx --debug shell: pwsh Deploy_GitHub_Release: name: Deploy App to GitHub Release needs: Deploy_Azure_Blob if: ${{ github.event_name != 'pull_request' }} environment: name: Deploy_GitHub_Release url: ${{ steps.deploy_staging.outputs.webapp-url }} runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Generate Changelog for Latest Commit id: changelog uses: jaywcjlove/changelog-generator@main with: token: ${{ secrets.GITHUB_TOKEN }} filter: '' env: commitMode: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download Standalone Signed App uses: actions/download-artifact@v4 with: name: Standalone_Signed path: ./StandaloneSigned - name: Download Release Signed App uses: actions/download-artifact@v4 with: name: Release_Signed path: ./ReleaseSigned - name: Download Build Artifacts uses: actions/download-artifact@v4 if: ${{ github.event_name == 'push' }} with: name: BuildArtifacts path: ./BuildArtifacts - name: Get Version from Artifact run: | version=$(<"./BuildArtifacts/version.txt") echo "VERSION=$version" >> $GITHUB_ENV - name: Add hashes for Standalone App run: | $zip64Hash = Get-FileHash "./StandaloneSigned/PresenceLight.${{ env.VERSION }}-x64.zip" -Algorithm SHA256 $zip64Hash.Hash | Out-File -Encoding 'UTF8' "./StandaloneSigned/PresenceLight.${{ env.VERSION }}-x64.zip.sha256" $zip86Hash = Get-FileHash "./StandaloneSigned/PresenceLight.${{ env.VERSION }}-x86.zip" -Algorithm SHA256 $zip86Hash.Hash | Out-File -Encoding 'UTF8' "./StandaloneSigned/PresenceLight.${{ env.VERSION }}-x86.zip.sha256" $zipARMHash = Get-FileHash "./StandaloneSigned/PresenceLight.${{ env.VERSION }}-win-arm64.zip" -Algorithm SHA256 $zipARMHash.Hash | Out-File -Encoding 'UTF8' "./StandaloneSigned/PresenceLight.${{ env.VERSION }}-win-arm64.zip.sha256" $appxHash = Get-FileHash "./ReleaseSigned/PresenceLight.Package_${{ env.VERSION }}.0_Test/PresenceLight.Package_${{ env.VERSION }}.0_x64_x86_ARM64.appxbundle" -Algorithm SHA256 $appxHash.Hash | Out-File -Encoding 'UTF8' "./ReleaseSigned/PresenceLight.Package_${{ env.VERSION }}.0_x64_x86_ARM64.appxbundle.sha256" shell: pwsh - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: "Desktop-v${{ env.VERSION }}" body: ${{ steps.changelog.outputs.changelog }} fail_on_unmatched_files: true token: ${{ secrets.GITHUB_TOKEN }} files: | StandaloneSigned/*.zip StandaloneSigned/*.sha256 ReleaseSigned/*.sha256 ReleaseSigned/**/*.appxbundle - name: Setup tmate session if: ${{ failure() }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 15 Deploy_Choco: name: Deploy Standalone App Chocolatey needs: Deploy_GitHub_Release if: ${{ github.event_name != 'pull_request' }} uses: isaacrlevin/presencelight/.github/workflows/Choco.yml@main secrets: inherit Deploy_Store: name: Deploy App to Windows Store needs: Deploy_Azure_Blob if: ${{ github.event_name != 'pull_request' }} environment: name: Deploy_Store url: ${{ steps.deploy_staging.outputs.webapp-url }} runs-on: ubuntu-latest steps: - name: Download Release Signed uses: actions/download-artifact@v4 with: name: Release_Signed path: ./ReleaseSigned - name: Upload Badges to Azure Blob Storage run: | az storage blob upload --account-key ${{ secrets.ACCOUNT_KEY }} --account-name ${{ secrets.ACCOUNT_NAME }} -f "./ReleaseSigned/stable_badge.svg" -n stable_badge.svg -c store --content-type image/svg+xml --debug --overwrite shell: pwsh - name: Windows Store Publish uses: isaacrlevin/windows-store-action@1.0 with: tenant-id: ${{ secrets.STORE_TENANT }} client-id: ${{ secrets.STORE_CLIENT_ID }} client-secret: ${{ secrets.STORE_CLIENT_SECRET }} app-id: ${{ secrets.APP_ID }} package-path: "./ReleaseSigned/" Deploy_Winget: name: Deploy App to WinGet needs: Deploy_GitHub_Release if: ${{ github.event_name != 'pull_request' }} uses: isaacrlevin/presencelight/.github/workflows/WinGet.yml@main secrets: inherit ================================================ FILE: .github/workflows/Deploy_Web.yml ================================================ on: push: branches: [ main ] paths-ignore: - '.github/workflows/Deploy_Desktop.yml' - 'src/DesktopClient/**' - 'src/**' - '*..md' - 'docs/*..md' - 'Build/**' - 'chocolatey/**' pull_request: branches: [ main ] paths-ignore: - '.github/workflows/Deploy_Desktop.yml' - 'src/DesktopClient/**' - 'src/**' - '*..md' - 'docs/*..md' - 'Build/**' - 'chocolatey/**' jobs: Setup_Web: name: Setup Web runs-on: ubuntu-latest env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true BuildConfiguration: Release ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Use .NET Core SDK 8.0.x and 10.0.x uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x 10.0.x - name: Nerdbank.GitVersioning uses: dotnet/nbgv@v0.4.2 with: setCommonVars: true - run: echo "BuildNumber - ${{ env.GitBuildVersionSimple }}" - name: Add Secrets to appsettings.json run: | ./Build/scripts/update-web-settings.ps1 -Version "${{ env.GitBuildVersionSimple }}" -ApplicationId "${{ secrets.ApplicationId }}" ` -ClientSecret "${{ secrets.ClientSecret }}" -InstrumentationKey "${{ secrets.InstrumentationKey }}" ` -LIFXClientId "${{ secrets.LIFXClientId }}" -LIFXClientSecret "${{ secrets.LIFXClientSecret }}" ` -RemoteHueClientId "${{ secrets.RemoteHueClientId }}" -RemoteHueClientSecret "${{ secrets.RemoteHueClientSecret }}" ` -RemoteHueClientAppName "${{ secrets.RemoteHueClientAppName }}" shell: pwsh if: ${{ success() && github.event_name != 'pull_request' }} - name: Create Version File to Artifact run : | New-Item -Path ./Build -Name "version.txt" -ItemType "file" -Value "${{ env.GitBuildVersionSimple }}" shell: pwsh - name: Publish PreBuild Arifacts uses: actions/upload-artifact@v4 with: path: ./src name: PreBuild - name: Publish Files for Build uses: actions/upload-artifact@v4 with: path: Build name: BuildArtifacts if: ${{ success() }} Build_Web: name: Build Web needs: Setup_Web runs-on: ubuntu-latest strategy: matrix: include: - ChannelName: Windows_x64_x86 RID: win-x64 - ChannelName: Windows_ARM RID: win-arm64 - ChannelName: macOS RID: osx-x64 - ChannelName: Linux_ARM RID: linux-arm - ChannelName: Linux_ARM64 RID: linux-x64 - ChannelName: Linux_Musl_x64 RID: linux-musl-x64 - ChannelName: Linux_Musl_ARM_x64 RID: linux-musl-arm64 env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true BuildConfiguration: Release ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: - name: Use .NET Core SDK 8.0.x and 10.0.x uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x 10.0.x - name: Download PreBuild uses: actions/download-artifact@v4 with: name: PreBuild path: ./src - name: Download Build Artifacts uses: actions/download-artifact@v4 with: name: BuildArtifacts path: ./BuildArtifacts - name: Get Version from Artifact run: | version=$(<"${{ github.workspace }}/BuildArtifacts/version.txt") echo "GitBuildVersionSimple=$version" >> $GITHUB_ENV - name: Create Directory for ${{ matrix.ChannelName }} Channel run: mkdir ./${{ matrix.ChannelName }} - name: dotnet publish ${{ matrix.RID }} run: dotnet publish './src/PresenceLight.Web/PresenceLight.Web.csproj' -r ${{ matrix.RID }} -c ${{ env.BuildConfiguration }} /p:PublishSingleFile=true -o ./PresenceLight.${{ env.GitBuildVersionSimple }}_${{ matrix.ChannelName }} /p:Version=${{ env.GitBuildVersionSimple }} --property WarningLevel=0 if: ${{ success() }} - name: Zip PresenceLight Web Files run: | Compress-Archive -Path './PresenceLight.${{ env.GitBuildVersionSimple }}_${{ matrix.ChannelName }}' ` -DestinationPath ./${{ matrix.ChannelName }}/PresenceLight.${{ matrix.ChannelName }}.${{ env.GitBuildVersionSimple }}.zip shell: pwsh - name: Publish ${{ matrix.ChannelName }} Arifacts uses: actions/upload-artifact@v4 with: path: ./${{ matrix.ChannelName }} name: ${{ matrix.ChannelName }} Code_Signing: name: Code Sign Worker needs: Build_Web strategy: matrix: target: [ Windows_x64_x86, Windows_ARM, macOS, Linux_ARM, Linux_ARM64, Linux_Musl_x64, Linux_Musl_ARM_x64 ] uses: isaacrlevin/presencelight/.github/workflows/Sign.yml@main with: target: ${{ matrix.target }} secrets: inherit Deploy_Azure_Blob: needs: Code_Signing name: Deploy Worker to Azure Blob Storage if: ${{ github.event_name != 'pull_request' }} strategy: matrix: target: [ Windows_x64_x86, Windows_ARM, macOS, Linux_ARM, Linux_ARM64, Linux_Musl_x64, Linux_Musl_ARM_x64 ] uses: isaacrlevin/presencelight/.github/workflows/Azure_Blob_Deploy.yml@main with: target: ${{ matrix.target }} secrets: inherit Deploy_Containers: name: Deploy Web Containers (DockerHub / GitHub Packages) needs: Build_Web if: ${{ github.event_name != 'pull_request' }} environment: name: Deploy_Containers url: ${{ steps.deploy_staging.outputs.webapp-url }} runs-on: ubuntu-latest steps: - name: Download PreBuild uses: actions/download-artifact@v4 with: name: PreBuild path: ./src - name: Download Build Artifacts uses: actions/download-artifact@v4 with: name: BuildArtifacts path: ./BuildArtifacts - name: Get Version from Artifact run: | version=$(<"./BuildArtifacts/version.txt") echo "VERSION=$version" >> $GITHUB_ENV - name: Update Docker Files run: | $dockerFileLatest = Get-Content -path "./src/PresenceLight.Web/Dockerfile" -Raw $dockerFileLatest = $dockerFileLatest -replace '{VERSION}', "${{ env.VERSION }} " $dockerFileLatest | Set-Content -Path "./src/PresenceLight.Web/Dockerfile" $dockerFile32 = Get-Content -path "./src/PresenceLight.Web/Dockerfile.debian-arm32" -Raw $dockerFile32 = $dockerFile32 -replace '{VERSION}', "${{ env.VERSION }} " $dockerFile32 | Set-Content -Path "./src/PresenceLight.Web/Dockerfile.debian-arm32" $dockerFile64 = Get-Content -path "./src/PresenceLight.Web/Dockerfile.debian-arm64" -Raw $dockerFile64 = $dockerFile64 -replace '{VERSION}', "${{ env.VERSION }} " $dockerFile64 | Set-Content -Path "./src/PresenceLight.Web/Dockerfile.debian-arm64" shell: pwsh if: ${{ success() && github.event_name != 'pull_request' }} - name: Push latest Container tag to GitHub Registry uses: opspresso/action-docker@master with: args: --docker env: USERNAME: isaacrlevin REGISTRY: "ghcr.io" PASSWORD: ${{ secrets.GH_PERSONAL_TOKEN }} DOCKERFILE: "./src/PresenceLight.Web/Dockerfile" IMAGE_NAME: "isaacrlevin/presencelight" TAG_NAME: "${{ env.VERSION }}" LATEST: "true" BUILD_PATH: "./src/" - name: Push ARM Container tag to GitHub Registry uses: opspresso/action-docker@master with: args: --docker env: USERNAME: isaacrlevin REGISTRY: "ghcr.io" PASSWORD: ${{ secrets.GH_PERSONAL_TOKEN }} DOCKERFILE: "./src/PresenceLight.Web/Dockerfile.debian-arm32" IMAGE_NAME: "isaacrlevin/presencelight" TAG_NAME: "debian-arm32" BUILD_PATH: "./src/" - name: Push ARM64 tag to GitHub Registry uses: opspresso/action-docker@master with: args: --docker env: USERNAME: isaacrlevin REGISTRY: "ghcr.io" PASSWORD: ${{ secrets.GH_PERSONAL_TOKEN }} DOCKERFILE: "./src/PresenceLight.Web/Dockerfile.debian-arm64" IMAGE_NAME: "isaacrlevin/presencelight" TAG_NAME: "debian-arm64" BUILD_PATH: "./src/" - name: Push ARM tagto DockerHub (Versioned) uses: opspresso/action-docker@master with: args: --docker env: USERNAME: ${{ secrets.DOCKER_USERNAME }} PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKERFILE: "./src/PresenceLight.Web/Dockerfile.debian-arm32" IMAGE_NAME: "isaaclevin/presencelight" TAG_NAME: "${{ env.VERSION }}-debian-arm32" BUILD_PATH: "./src/" - name: Push ARM tag to DockerHub (Latest) uses: opspresso/action-docker@master with: args: --docker env: USERNAME: ${{ secrets.DOCKER_USERNAME }} PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKERFILE: "./src/PresenceLight.Web/Dockerfile.debian-arm32" IMAGE_NAME: "isaaclevin/presencelight" TAG_NAME: "debian-arm32" BUILD_PATH: "./src/" - name: Push ARM64 tag to DockerHub (Versioned) uses: opspresso/action-docker@master with: args: --docker env: USERNAME: ${{ secrets.DOCKER_USERNAME }} PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKERFILE: "./src/PresenceLight.Web/Dockerfile.debian-arm64" IMAGE_NAME: "isaaclevin/presencelight" TAG_NAME: "${{ env.VERSION }}-debian-arm64" BUILD_PATH: "./src/" - name: Push ARM64 tag to DockerHub (Latest) uses: opspresso/action-docker@master with: args: --docker env: USERNAME: ${{ secrets.DOCKER_USERNAME }} PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKERFILE: "./src/PresenceLight.Web/Dockerfile.debian-arm64" IMAGE_NAME: "isaaclevin/presencelight" TAG_NAME: "debian-arm64" BUILD_PATH: "./src/" - name: Push latest tag to DockerHub uses: opspresso/action-docker@master with: args: --docker env: USERNAME: ${{ secrets.DOCKER_USERNAME }} PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKERFILE: "./src/PresenceLight.Web/Dockerfile" IMAGE_NAME: "isaaclevin/presencelight" TAG_NAME: "${{ env.VERSION }}" LATEST: "true" BUILD_PATH: "./src/" Deploy_GitHub_Release: name: Deploy Web to GitHub Release needs: Deploy_Azure_Blob if: ${{ github.event_name != 'pull_request' }} environment: name: Deploy_GitHub_Release url: ${{ steps.deploy_staging.outputs.webapp-url }} runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Generate Changelog id: changelog uses: jaywcjlove/changelog-generator@main with: token: ${{ secrets.GITHUB_TOKEN }} filter: '' env: commitMode: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download Windows_x64_x86 Artifacts uses: actions/download-artifact@v4 with: name: Windows_x64_x86 path: ./Sign/Windows_x64_x86 - name: Download Windows_ARM Artifacts uses: actions/download-artifact@v4 with: name: Windows_ARM path: ./Sign/Windows_ARM - name: Download macOS Artifacts uses: actions/download-artifact@v4 with: name: macOS path: ./Sign/macOS - name: Download Linux_ARM Artifacts uses: actions/download-artifact@v4 with: name: Linux_ARM path: ./Sign/Linux_ARM - name: Download Linux_ARM64 Artifacts uses: actions/download-artifact@v4 with: name: Linux_ARM64 path: ./Sign/Linux_ARM64 - name: Download Linux_Musl_x64 Artifacts uses: actions/download-artifact@v4 with: name: Linux_Musl_x64 path: ./Sign/Linux_Musl_x64 - name: Download Linux_Musl_ARM_x64 Artifacts uses: actions/download-artifact@v4 with: name: Linux_Musl_ARM_x64 path: ./Sign/Linux_Musl_ARM_x64 - name: Download Build Artifacts uses: actions/download-artifact@v4 with: name: BuildArtifacts path: "./BuildArtifacts" - name: Get Version from Artifact run: | version=$(<"./BuildArtifacts/version.txt") echo "VERSION=$version" >> $GITHUB_ENV - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: "Web-v${{ env.VERSION }}" body: ${{ steps.changelog.outputs.changelog }} fail_on_unmatched_files: true token: ${{ secrets.GITHUB_TOKEN }} files: | ./Sign/**/*.zip - name: Setup Tmate session if: ${{ failure() }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 15 ================================================ FILE: .github/workflows/Sign.yml ================================================ name: Code Sign App on: workflow_dispatch: inputs: target: description: 'Target of Channel Name to Sign' required: true default: '' type: string workflow_call: inputs: target: description: 'Target of Channel Name to Sign' required: true default: '' type: string jobs: Sign_Code: name: Sign ${{ inputs.target }} App permissions: id-token: write # Required for requesting the JWT runs-on: windows-latest steps: - name: Download ${{ inputs.target }} Artifacts uses: actions/download-artifact@v4 with: name: ${{ inputs.target }} path: .\ToSign\${{ inputs.target }} - name: Download Build Artifacts uses: actions/download-artifact@v4 with: name: BuildArtifacts path: .\BuildArtifacts - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x 10.0.x - name: Install Code Sign CLI tool # run: dotnet tool install --tool-path . sign --version 0.9.0-beta.23063.3 run: dotnet tool install --tool-path . --prerelease sign - name: Azure Login uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Run Code Signing CLI shell: pwsh run: > ./sign code azure-key-vault '**/*.{exe,zip,appxbundle,appinstaller}' --timestamp-url "http://timestamp.digicert.com" --base-directory "${{ github.workspace }}\ToSign" --file-list "${{ github.workspace }}\BuildArtifacts\Signing\filelist.txt" --publisher-name "Isaac Levin" --description "PresenceLight" --description-url "https://github.com/isaacrlevin/presencelight" --azure-key-vault-managed-identity true --azure-key-vault-url "${{ secrets.KEY_VAULT_URL }}" --azure-key-vault-certificate "${{ secrets.KEY_VAULT_CERTIFICATE_ID }}" --verbosity Trace - name: Publish Signed ${{ inputs.target }} Packages uses: actions/upload-artifact@v4 with: path: .\ToSign\${{ inputs.target }} name: '${{ inputs.target }}_Signed' - name: Setup Tmate session if: ${{ failure() }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 15 ================================================ FILE: .github/workflows/WinGet.yml ================================================ name: Winget Publish on: workflow_dispatch: inputs: Version: description: 'Release' required: true default: '5.0' type: string workflow_call: jobs: publish: runs-on: windows-latest name: Publish App to Winget env: WINGETCREATE_TOKEN: ${{ secrets.WINGETCREATE_TOKEN }} steps: - name: Download Artifacts for Winget Publish uses: actions/download-artifact@v4 if: ${{ github.event_name == 'push' }} with: name: BuildArtifacts path: .\BuildArtifacts - name: Set VERSION Environment Variable (PUSH) if: ${{ github.event_name == 'push' }} run: | $version = Get-Content ".\BuildArtifacts\version.txt" echo "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append shell: pwsh - name: Set VERSION Environment Variable (Workflow Dispatch) if: ${{ github.event_name == 'workflow_dispatch' }} run: | version = "${{ inputs.Version }}" echo "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append shell: pwsh - name: Publish App to Winget working-directory: ${{ github.workspace }}\BuildArtifacts\scripts run: | .\push-winget.ps1 -Version "${{ env.VERSION }}" -Token "${{ secrets.WINGETCREATE_TOKEN }}" shell: pwsh - name: Setup Tmate session if: ${{ failure() }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 15 ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/visualstudio,windows # Edit at https://www.gitignore.io/?templates=visualstudio,windows ### Windows ### # Windows thumbnail cache files Thumbs.db Thumbs.db:encryptable 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 ### VisualStudio ### ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # 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/ win-arm64/ Nightly/ .Store SignClient.exe [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015/2017 cache/options directory .vs/ .vscode/ # 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 Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.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 # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # 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 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/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # End of https://www.gitignore.io/api/visualstudio,windows /src/lib/RCWPF/2020.1.218.45.NoXaml.Trial /sign.ps1 /src/DesktopClient/PresenceLight/appsettings.Development.json /src/PresenceLight.Web/PresenceLightSettings.Development.json /src/PresenceLight.Web/appsettings.Development.json src/local-action-test-wpf.ps1 .github/workflows/test.yml Build/StorePublish/.env.local Build/StorePublish/node_modules Build/StorePublish/temp.zip /src/DesktopClient/PresenceLight/Properties/launchSettings.json /src/PresenceLight.Web/config/ logs/ /src/config .ionide src/DesktopClient/PresenceLight/settings.json settings.json ================================================ FILE: Build/Signing/filelist.txt ================================================ **/PresenceLight.* ================================================ FILE: Build/Worker/presencelight.crt ================================================ -----BEGIN CERTIFICATE----- MIICBTCCAasCFBvttCTh9n9rqOSzRE1jFBdeRyRXMAoGCCqGSM49BAMCMIGEMQsw CQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEUMBIGA1UEBwwLV29vZGlu dmlsbGUxFDASBgNVBAoMC0lzYWFjIExldmluMRYwFAYDVQQDDA1wcmVzZW5jZWxp Z2h0MRwwGgYJKoZIhvcNAQkBFg10ZXN0QHRlc3QuY29tMB4XDTIwMDUyNDIxNTAz MloXDTIxMDUyNDIxNTAzMlowgYQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNo aW5ndG9uMRQwEgYDVQQHDAtXb29kaW52aWxsZTEUMBIGA1UECgwLSXNhYWMgTGV2 aW4xFjAUBgNVBAMMDXByZXNlbmNlbGlnaHQxHDAaBgkqhkiG9w0BCQEWDXRlc3RA dGVzdC5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQmWh86vQxjFjlNHt/6 8PD9Jg2TJIvaceGaiq+2t/CKoG0FEeiHUYwiozztU6Ad5+dp25OSUzqz2JFy/N+J iI+2MAoGCCqGSM49BAMCA0gAMEUCIFnvmXTnJQNmPCS6QDVYLrFIYTx3Gzi1nTVD h5n//+/7AiEAlUxK5U45oJtK2CuHcwquxzar8eB9ZcBAydfuGA9G7o8= -----END CERTIFICATE----- ================================================ FILE: Build/Worker/presencelight.service ================================================ [Unit] Description=PresenceLight is a solution to broadcast your various statuses to a Philips Hue or LIFX light bulb. [Service] WorkingDirectory=/home/pi/PresenceLight ExecStart=/home/pi/PresenceLight/PresenceLight Restart=always # Restart service after 10 seconds if the dotnet service crashes: RestartSec=10 KillSignal=SIGINT SyslogIdentifier=PresenceLight User=pi Environment=ASPNETCORE_ENVIRONMENT=Production Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false [Install] WantedBy=multi-user.target ================================================ FILE: Build/Worker/trust-cert.sh ================================================ pk12util -d sql:$HOME/.pki/nssdb -i presencelight.pfx certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n 'presencelight' -i presencelight.crt ================================================ FILE: Build/scripts/push-choco.ps1 ================================================ Param ( [parameter(Mandatory = $true)] [string] $Version, [parameter(Mandatory = $true)] [string] $CHOCOAPIKEY ) function Hash-Files { param ( [parameter(Mandatory = $true)] [string] $Version ) # Get the latest release from GitHub $github = Invoke-RestMethod -uri "https://api.github.com/repos/isaacrlevin/presencelight/releases" $targetRelease = $github | Where-Object -Property name -match "Desktop-v$Version" | Select-Object -First 1 mkdir .\Download $fileNames = (, "x86", "x64", "win-arm64") # Get the Hashes from the GitHub Release and save them to files $hashUrls = $targetRelease | Select-Object -ExpandProperty assets -First 1 | Where-Object -Property name -match '.*?.zip.sha256' | Select-Object -ExpandProperty browser_download_url foreach ($url in $hashUrls) { foreach ($fileName in $fileNames) { if ($url -like "*$fileName*") { $filePath = ".\Download\$fileName.zip.sha256" Invoke-WebRequest -Uri $url -OutFile $filePath break } } } $hash86 = get-content ".\Download\x86.zip.sha256" $hash64 = get-content ".\Download\x64.zip.sha256" $hashARM = get-content ".\Download\win-arm64.zip.sha256" # Update ChocolateyInstall.ps1 with Hashes $installFile = Get-Content -path ".\Chocolatey\tools\ChocolateyInstall.ps1" -Raw $installFile = $installFile -replace '{ReplaceCheckSumARM}', $hashARM $installFile = $installFile -replace '{ReplaceCheckSumx86}', $hash86 $installFile = $installFile -replace '{ReplaceCheckSumx64}', $hash64 # Update Verification.txt with Hashes $verificationFile = Get-Content -path ".\Chocolatey\tools\Verification.txt" $verificationFile = $verificationFile -replace '{HASHx64}', $hash64 $verificationFile = $verificationFile -replace '{HASHx86}', $hash86 $verificationFile = $verificationFile -replace '{HASHARM}', $hashARM # Get the Download Urls for the Zip files and update ChocolateyInstall.ps1 and Verification.txt with Urls $zipUrls = $targetRelease | Select-Object -ExpandProperty assets | Where-Object { $_.name -like '*.zip' } | Select-Object -ExpandProperty browser_download_url foreach ($url in $zipUrls) { if ($url -like "*x64*") { $installFile = $installFile -replace '{x64Link}' , $url $verificationFile = $verificationFile -replace '{x64Link}' , $url } if ($url -like "*x86*") { $installFile = $installFile -replace '{x86Link}' , $url $verificationFile = $verificationFile -replace '{x86Link}' , $url } if ($url -like "*arm64*") { $installFile = $installFile -replace '{ARMLink}' , $url $verificationFile = $verificationFile -replace '{ARMLink}' , $url } } # Save the updated files $verificationFile | Set-Content -Path ".\Chocolatey\tools\Verification.txt" $installFile | Set-Content -Path ".\Chocolatey\tools\ChocolateyInstall.ps1" } Hash-Files -Version $Version # Chocolatey Pack & choco.exe pack ".\Chocolatey\PresenceLight.nuspec" --version "${Version}" --OutputDirectory ".\Chocolatey" $CHOCOAPIKEY = $CHOCOAPIKEY -replace "`n", "" -replace "`r", "" -replace " ", "" & choco.exe apikey --key "${CHOCOAPIKEY}" --source https://push.chocolatey.org/ $nupkgs = gci ".\Chocolatey\PresenceLight.*.nupkg" | Select -ExpandProperty FullName foreach ($nupkg in $nupkgs) { & choco.exe push $nupkg --source https://push.chocolatey.org/ --debug --verbose } ================================================ FILE: Build/scripts/push-winget.ps1 ================================================ Param ( [parameter(Mandatory = $true)] [string] $Version, [parameter(Mandatory = $false)] [string] $Token ) $github = Invoke-RestMethod -uri "https://api.github.com/repos/isaacrlevin/presencelight/releases" $targetRelease = $github | Where-Object -Property name -match "Desktop-v$Version" | Select-Object -First 1 $installerUrl = $targetRelease | Select-Object -ExpandProperty assets | Where-Object { $_.name -like '*.appxbundle' } | Select-Object -ExpandProperty browser_download_url # Update package using wingetcreate Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe .\wingetcreate.exe update "isaaclevin.presencelight" --version $Version --urls "$installerUrl" --submit --token $Token ================================================ FILE: Build/scripts/update-desktop-settings.ps1 ================================================ Param ( [parameter(Mandatory = $true)] [string] $Release, [parameter(Mandatory = $true)] [string] $Version, [parameter(Mandatory = $true)] [string] $ApplicationId, [parameter(Mandatory = $true)] [string] $ClientSecret, [parameter(Mandatory = $true)] [string] $InstrumentationKey, [parameter(Mandatory = $true)] [string] $LIFXClientId, [parameter(Mandatory = $true)] [string] $LIFXClientSecret, [parameter(Mandatory = $true)] [string] $RemoteHueClientId, [parameter(Mandatory = $true)] [string] $RemoteHueClientSecret, [parameter(Mandatory = $true)] [string] $RemoteHueClientAppName ) switch ($Release) { "Release" { Write-Host "Updating AppxManifest for Release" $xmlPath= Resolve-Path ".\src\DesktopClient\PresenceLight.Package\Package.appxmanifest" Write-Host "Updating AppxManifest for Release" [xml]$manifest = get-content $xmlPath $manifest.Package.Identity.Version = "${Version}.0" $manifest.save($xmlPath) } "Nightly" { Write-Host "Updating AppxManifest for Nightly" $xmlPath= Resolve-Path ".\src\DesktopClient\PresenceLight.Package\Package-Nightly.appxmanifest" Write-Host "Updating AppxManifest for Nightly" [xml]$manifest = get-content $xmlPath $manifest.Package.Identity.Version = "${Version}.0" $manifest.save($xmlPath) } "Standalone" { } } Write-Host "Updating AppSettings for All Channels" $appsettings = get-content ".\src\DesktopClient\PresenceLight\appsettings.json" -raw | ConvertFrom-Json $appsettings.aadSettings.clientId = "${ApplicationId}" $appsettings.appVersion = "${Version}" $appsettings.lightSettings.lifx.LIFXClientId = "${LIFXClientId}" $appsettings.lightSettings.lifx.LIFXClientSecret = "${LIFXClientSecret}" $appsettings.applicationInsights.instrumentationkey = "${InstrumentationKey}" $appsettings.lightSettings.hue.RemoteHueClientId = "${RemoteHueClientId}" $appsettings.lightSettings.hue.RemoteHueClientSecret = "${RemoteHueClientSecret}" $appsettings.lightSettings.hue.RemoteHueClientAppName = "${RemoteHueClientAppName}" $appsettings | ConvertTo-Json -depth 32 | set-content '.\src\DesktopClient\PresenceLight\appsettings.json' ================================================ FILE: Build/scripts/update-web-settings.ps1 ================================================ Param ( [parameter(Mandatory = $true)] [string] $Version, [parameter(Mandatory = $true)] [string] $ApplicationId, [parameter(Mandatory = $true)] [string] $ClientSecret, [parameter(Mandatory = $true)] [string] $InstrumentationKey, [parameter(Mandatory = $true)] [string] $LIFXClientId, [parameter(Mandatory = $true)] [string] $LIFXClientSecret, [parameter(Mandatory = $true)] [string] $RemoteHueClientId, [parameter(Mandatory = $true)] [string] $RemoteHueClientSecret, [parameter(Mandatory = $true)] [string] $RemoteHueClientAppName ) # Update AppSettings.json. This must be done before build. $appsettings = get-content ".\src\PresenceLight.Web\appsettings.json" -raw | ConvertFrom-Json $appsettings.AADSettings.clientId = $ApplicationId $appsettings.AADSettings.clientSecret = $ClientSecret $appsettings.appVersion = $GitBuildVersionSimple $appsettings.applicationInsights.instrumentationkey = $InstrumentationKey $appsettings | ConvertTo-Json -depth 32 | set-content '.\src\PresenceLight.Web\appsettings.json' # Update PresenceLightSettings.json. This must be done before build. $PresenceLightSettings = get-content ".\src\PresenceLight.Web\PresenceLightSettings.json" -raw | ConvertFrom-Json $PresenceLightSettings.lightSettings.lifx.LIFXClientId = $LIFXClientId $PresenceLightSettings.lightSettings.lifx.LIFXClientSecret = $LIFXClientSecret $PresenceLightSettings.lightSettings.hue.RemoteHueClientId = $RemoteHueClientId $PresenceLightSettings.lightSettings.hue.RemoteHueClientSecret = $RemoteHueClientSecret $PresenceLightSettings.lightSettings.hue.RemoteHueClientAppName = $RemoteHueClientAppName $PresenceLightSettings | ConvertTo-Json -depth 32 | set-content '.\src\PresenceLight.Web\PresenceLightSettings.json' ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Isaac Levin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: PrivacyPolicy.md ================================================ # Information Collected And Transmitted By PresenceLight First, a reminder: PresenceLight is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. With that out of the way, here's a breakdown of all the information we may collect via Application Insights. ### Application-Level Includes: * Exception information * Could, in rare cases, contain paths packages on your computer * Machine name * Host name * Version number (e.g. 2.0.x.x) ### Operating System-Level Includes: * Architecture (e.g. 32-bit) * Version (e.g. Windows 10.0.17763.0) * Build (e.g. 17134.1.amd64fre.rs4_release.180410-1804) * Available processors/cores (e.g. 8 cores) * Machine Name (e.g. MyFastPC) * .NET Core Common Language Runtime version (e.g. 4.0.30319.42000) ## Package Sources **OS information and IP address** When Presence Light makes calls to authenticate users, it uses the Microsoft Graph Api. The author of PresenceLight does not have access to this information, but Microsoft Graph does and logs this information. **3rd-party package source** When user specifies a different package source than the default source at http://nuget.org, he/she will be subjected to the privacy policy of that website. NuGet Package Explorer does not send any such data to its author. ## Third-Party Policies * LIFX https://www.lifx.com/pages/privacy-policy * Philips Hue https://www2.meethue.com/en-us/support/privacy-policy * Microsoft Store https://docs.microsoft.com/en-us/legal/windows/agreements/store-policies ================================================ FILE: README.md ================================================ ![Logo](https://github.com/isaacrlevin/PresenceLight/raw/main/Icon.png) # PresenceLight ### NOTE: Due to internal changes at Microsoft, the Web/Container Version no longer works. I am currently looking into resolving this issue, but in the meantime, you will have to create an App Registration yourself and build the code on your own. :( ![.github/workflows/Deploy_Web.yml](https://github.com/isaacrlevin/presencelight/workflows/.github/workflows/Deploy_Web.yml/badge.svg) ![.github/workflows/Deploy_Desktop.yml](https://github.com/isaacrlevin/presencelight/workflows/.github/workflows/Deploy_Desktop.yml/badge.svg) ## Get PresenceLight ### Desktop Version | Nightly | Microsoft Store | Chocolatey | GitHub Releases | | ------- | --------------- | ---------- | --------------- | | [](https://presencelight.blob.core.windows.net/nightly/index.html)| [](https://www.microsoft.com/en-us/p/presencelight/9nffkd8gznl7) | [](https://chocolatey.org/packages/PresenceLight/) | [](https://github.com/isaacrlevin/presencelight/releases) | ## Web Version |Web Download Site | Web Container from DockerHub | Web Container from GitHub Registry | ------- | --------------- | --------------- | [](https://presencelightapp.azurewebsites.net/) | [](https://hub.docker.com/r/isaaclevin/presencelight) | [](https://github.com/users/isaacrlevin/packages/container/package/presencelight) | ## App Versions | Application Type | Platforms | Readme |--- | ---- | ---- | | Desktop (.NET 10) | Windows 10 (min Version 1803) / Windows 11 | [Desktop Readme](docs/desktop-README.md) | Web (ASP.NET 10) | Windows, MacOS, Linux (Debian, AMD x64, ARM, ARM x64), | [Web Readme](docs/web-README.md) ## What is PresenceLight? [PresenceLight](https://isaacl.dev/presence-light) is a solution to broadcast your various statuses to various kinds of smart lights. Some statuses you can broadcast are: your availability in Microsoft Teams or color of your choosing. There are other solutions that do something similar to sending Teams Availability to a light, but they require a tethered solution (plugging a light into a computer via USB). What PresenceLight does is leverage the [Presence Api](https://docs.microsoft.com/graph/api/presence-get), which is available in [Microsoft Graph](https://docs.microsoft.com/graph/overview), allowing to retrieve your presence without having to be tethered. This could potentially allow someone to update the light bulb from a remote machine they do not use. #### [Blog Post](https://isaacl.dev/presence-light) #### [PresenceLight Demos](https://www.youtube.com/playlist?list=PL_IEvQa-oTVtB3fKUclJNNJ1r-Sxtjc-m) ## Supported Hardware | Light Type | | ------------ | | Philips Hue (Local and Remote) | LIFX | | Yeelight | | Philips Wiz | | [WLED](https://kno.wled.ge/) (via serial or web API) | | Any light which can be controlled via a GET or POST call to a web API | ## Docs - [Configure Hardware](docs/configure-hardware.md) - [FAQ](docs/faq.mdFAQ) - [Configure Custom Api Endpoint](docs/configure-custom-api.md) - [Configure Microsft Entra ID App (OPTIONAL)](/docs/configure-entra-app.md) ## Please Contribute I welcome all contributions here! Before you do, please read the [Contributors Guide](docs/CONTRIBUTING.md) ## Third Party Libraries Presence Light would not be possible without the amazing work from the contributors to the following third party libraries! - Lights - [Q42.HueApi](https://github.com/Q42/Q42.HueApi) - [OpenWiz](https://github.com/UselessMnemonic/OpenWiz) - [YeelightAPI](https://github.dev/roddone/YeelightAPI) - [LifxCloud](https://github.com/isaacrlevin/LifxCloudClient) - UI Components - [MudBlazor](https://www.mudblazor.com/) - [Blazorise](https://github.com/Megabit/Blazorise) - [BlazorPro.Spinkit](https://github.com/EdCharbeneau/BlazorPro.Spinkit) - Backend - [MediatR](https://github.com/jbogard/MediatR) - [Polly](https://github.com/App-vNext/Polly) - [Serilog](https://github.com/serilog/serilog) - [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) - [IdentityModel.OidcClient](https://github.com/IdentityModel/IdentityModel.OidcClient) - [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) ================================================ FILE: chocolatey/PresenceLight.nuspec ================================================ PresenceLight $version$ PresenceLight Isaac Levin Isaac Levin https://github.com/isaacrlevin/PresenceLight https://github.com/isaacrlevin/presencelight/blob/main/README.md https://github.com/isaacrlevin/PresenceLight/issues https://github.com/isaacrlevin/PresenceLight/blob/main/LICENSE https://github.com/isaacrlevin/PresenceLight/ https://github.com/isaacrlevin/PresenceLight/ https://rawcdn.githack.com/isaacrlevin/PresenceLight/2c388db6a155cf7bbc8b12578222e0609d147ee0/Icon.png false PresenceLight is a solution to broadcast your various statuses to a Philips Hue or LIFX light bulb. Some statuses you can broadcast are: your availability in Microsoft Teams, your current Windows 10 theme, and a theme or color of your choosing. ## Package Parameters - InstallDir: Change installation directory of PresenceLight. Default is "$env:APPDATA\PresenceLight" Broadcasts colors to various Smart Lights https://github.com/isaacrlevin/PresenceLight/releases presence lifx hue philips teams ================================================ FILE: chocolatey/tools/ChocolateyBeforeModify.ps1 ================================================ # Make sure to kill any presencelight processes before attempting an # uninstall or upgrade of PresenceLight Get-Process presencelight* -ErrorAction SilentlyContinue | Stop-Process ================================================ FILE: chocolatey/tools/ChocolateyInstall.ps1 ================================================ $ErrorActionPreference = 'Stop'; # Make sure to kill any presencelight processes before attempting an # installation. This covers the case that PresenceLight is currently # installed outside of Chocolatey Get-Process presencelight* -ErrorAction SilentlyContinue | Stop-Process $WindowsVersion=[Environment]::OSVersion.Version if ($WindowsVersion.Major -ne "10") { throw "This package requires Windows 10." } $IsCorrectBuild=[Environment]::OSVersion.Version.Build if ($IsCorrectBuild -lt "17134") { throw "This package requires at least Windows 10 version build 17134.x." } $packageName = "presencelight" $toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" $InstallDir = Join-Path $env:APPDATA 'PresenceLight' $pp = Get-PackageParameters if($pp['InstallDir']){ $InstallDir = $pp['InstallDir'] } $packageArgs = @{ packageName = $packageName unzipLocation = $InstallDir urlARM = "{ARMLink}" url86bit = "{x86Link}" url64bit = "{x64Link}" checksumARM = "{ReplaceCheckSumARM}" checksum = "{ReplaceCheckSumx86}" checksum64 = "{ReplaceCheckSumx64}" checksumType = 'SHA256' } Install-ChocolateyZipPackage @packageArgs $exePath = Join-Path $InstallDir 'PresenceLight.exe' Write-Output "Adding shortcut to Start Menu" Install-ChocolateyShortcut -ShortcutFilePath "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\PresenceLight.lnk" -TargetPath $exePath -WorkingDirectory $InstallDir Write-Output "Adding shortcut to Startup" Install-ChocolateyShortcut -ShortcutFilePath "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\PresenceLight.lnk" -TargetPath $exePath -WorkingDirectory $InstallDir ================================================ FILE: chocolatey/tools/ChocolateyUninstall.ps1 ================================================ # This logic can be removed after a couple of package version releases, since # this will be handled in the ChocolateyBeforeModify.ps1 going forward $light = Get-process presencelight* if($light){ $light | Stop-Process -Force } Remove-Item $Env:AppData\PresenceLight -Recurse -Force Remove-Item "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\PresenceLight.lnk" Remove-Item "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\PresenceLight.lnk" ================================================ FILE: chocolatey/tools/LICENSE.txt ================================================ MIT License Copyright (c) 2023 Isaac Levin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: chocolatey/tools/Verification.txt ================================================ VERIFICATION Verification is intended to assist the Chocolatey moderators and community in verifying that this package's contents are trustworthy. The package has been generated by our CI system and binaries/scripts signed with Authenticode. 1. Download Zipped Application with below Url Package Zip Urls - xARM Link: {ARMLink} - x86 Link: {x86Link} - x64 Link: {x64Link} 2. You can use one of the following methods to obtain the checksum - Use powershell function 'Get-Filehash' - Use chocolatey utility 'checksum.exe' checksum type: sha256 checksum: {HASHx86} checksum64: {HASHx64} checksumARM: {HASHARM} ================================================ FILE: docker-compose-example.yml ================================================ version: '3.7' services: presencelight: image: isaaclevin/presencelight:latest container_name: presencelight restart: unless-stopped ports: - 5000:80 - 5001:443 volumes: - /mnt/c/Users/isaac/.aspnet/https:/https:ro environment: ASPNETCORE_HTTPS_PORT: "5001" ASPNETCORE_URLS: "https://+;http://+" ASPNETCORE_Kestrel__Certificates__Default__Password: "presencelight" ASPNETCORE_Kestrel__Certificates__Default__Path: "/https/presencelight.pfx" ================================================ FILE: docs/CONTRIBUTING.md ================================================ # Contributing to PresenceLight Thank you for your interest in contributing to the PresenceLight project! We welcome contributions from the community to help improve and enhance the project. This guide will provide you with the necessary information to get started. ## Table of Contents - [Contributing to PresenceLight](#contributing-to-presencelight) - [Table of Contents](#table-of-contents) - [Getting Started](#getting-started) - [Setting Up Local Environment](#setting-up-local-environment) - [Obtain Microsoft Entra Client ID.](#obtain-microsoft-entra-client-id) - [Debugging Windows App](#debugging-windows-app) - [Debugging Web App](#debugging-web-app) - [Adding New Functionality](#adding-new-functionality) - [Contributing Guidelines](#contributing-guidelines) - [Submitting a Pull Request](#submitting-a-pull-request) - [Code of Conduct](#code-of-conduct) - [License](#license) ## Getting Started To contribute to PresenceLight, you will need to have the following prerequisites: - Basic knowledge of Git and GitHub. - Knowledge of the .NET framework and C# programming language (this project uses the latest version, [.NET 10](https://dot.net)). - IDE of choice ([Visual Studio 2022](https://visualstudio.microsoft.com/downloads/), [Visual Studio Code](https://code.visualstudio.com/Download), [JetBrains Rider](https://www.jetbrains.com/rider/download)) - [Docker Desktop](https://www.docker.com/products/docker-desktop/) if you want to test the Web project running as a container. ## Setting Up Local Environment ### Obtain Microsoft Entra Client ID. PresenceLight WILL not work if you try to clone and run, because there is a dependency on Microsoft Entra. Because of this, if you want to contribute to the project at this time, please reach out to the maintainer, [Isaac Levin](mailto:isaac@isaaclevin.com) to obtain the Client ID and Client Secret. Once obtained, the Client ID will need to be placed in 2 locations. Firstly, create create a copy of appsettings.json in both the Desktop and Web Projects, calling this new file `appsettings.Development.json`. Place the Client Id, in the proper locations in each file - [Desktop Version](https://github.com/isaacrlevin/presencelight/blob/main/src/DesktopClient/PresenceLight/appsettings.json#L13) - [Web Version](https://github.com/isaacrlevin/presencelight/blob/main/src/PresenceLight.Web/appsettings.json#L6) If you have access to your own Microsoft Entra tenant, you can also create your own Entra App and use the Client ID as well. More information on that is [here](configure-entra-app.md). ### Debugging Windows App After this, the app should build and run without issue. You can run the Desktop version as either a standalone .NET WPF app or as a packaged app that is deployed locally to the Windows store. - To debug standalone version, set [PresenceLight](https://github.com/isaacrlevin/presencelight/blob/main/src/DesktopClient/PresenceLight/PresenceLight.csproj) as startup project - To debug Microsoft Store version, set to [PresenceLight.Package](https://github.com/isaacrlevin/presencelight/blob/main/src/DesktopClient/PresenceLight.Package/PresenceLight.Package.wapproj) - NOTE: You may need to enable additional things in Visual Studio to make this work. More info [here](https://learn.microsoft.com/en-us/visualstudio/debugger/debug-installed-app-package) ### Debugging Web App After adding the [Client ID](https://github.com/isaacrlevin/presencelight/blob/main/src/PresenceLight.Web/appsettings.json#L6) and [Client Secret](https://github.com/isaacrlevin/presencelight/blob/de14b62d0e6b433735eef653cee48d550747b60d/src/PresenceLight.Web/appsettings.json#L10), you should be able to debug the web version by setting the [Web Project](https://github.com/isaacrlevin/presencelight/blob/main/src/PresenceLight.Web/PresenceLight.Web.csproj) as startup. ## Adding New Functionality If you are adding new functionality to PresenceLight (adding support for a new light for instance), there are a handful of steps you will need to take to enable the functionality in all versions. To understand what you need to do, it would be helpful to understand what all projects in the solution do. - ### [PresenceLight.Core](https://github.com/isaacrlevin/presencelight/tree/main/src/PresenceLight.Core) This project holds all the shared logic for PresenceLight, including interfacing with Microsoft Entra, Microsoft Graph, all lights, as well as all the models that exist for the solution. More than likely, you will be working inside the [Lights](https://github.com/isaacrlevin/presencelight/tree/main/src/PresenceLight.Core/Lights) folder in this project to add a new folder to include the code to support your feature. The project uses [MediatR](https://github.com/jbogard/MediatR) to send messages in-process across the application. Be sure to follow the existing pattern when adding Requests and Handlers - ### [PresenceLight.Razor]((https://github.com/isaacrlevin/presencelight/tree/main/src/PresenceLight.Razor)) This project holds all of the UI for the application, and leverages ASP.NET Core Blazor components to achieve this. If you are adding new functionality, you will either update an existing component or add a new one. If you are adding a new component, you will add a new `.razor` file in the [Pages](https://github.com/isaacrlevin/presencelight/tree/main/src/PresenceLight.Razor/Components/Pages) folder and add an entry in the `NavMenu.razor` for your new component. Please follow the existing patterns that you see in the other `.razor` files. - ### [PresenceLight](https://github.com/isaacrlevin/presencelight/tree/main/src/DesktopClient/PresenceLight) This is the WPF project that runs the desktop version of the application. The application contains a single Window that runs all of the functionality (calling the Graph API, calling handlers to update lights). Once you are ready to test your functionality for the Desktop version, add code to light up the functionality in [`MainWindow.xaml.cs`](https://github.com/isaacrlevin/presencelight/blob/main/src/DesktopClient/PresenceLight/MainWindow.xaml.cs). - ### [PresenceLight.Package](https://github.com/isaacrlevin/presencelight/tree/main/src/DesktopClient/PresenceLight.Package) This project wires up the WPF project to run in the Microsoft store. You should not need to update or add anything to this project. - ### [PresenceLight.Web](https://github.com/isaacrlevin/presencelight/tree/main/src/PresenceLight.Web) This is the project that runs the web version of the application. The project leverages a ASP.NET Core Worker Service to run the functionality that "polls" (calling Graph API, calling handlers for lights). Once you are ready to test your functionality for the Web version, add code to light up the functionality in [`Worker.cs`](https://github.com/isaacrlevin/presencelight/blob/main/src/PresenceLight.Web/Worker.cs). ## Contributing Guidelines Before you start contributing, please take a moment to review the following guidelines: 1. Fork the repository and create a new branch for your contribution. 2. Make sure your code follows the project's coding style and conventions. 3. Write clear and concise commit messages. 4. Test your changes thoroughly before submitting a pull request. It is important if you are adding new functionality to test both the Desktop AND Web versions. 5. Document any new features or changes in the project's documentation. ## Submitting a Pull Request Once you have made your changes and are ready to submit a pull request, follow these steps: 1. Push your changes to your forked repository. 2. Go to the original repository and create a new pull request. 3. Provide a clear and descriptive title for your pull request. 4. Include a detailed description of the changes you have made. 5. Wait for the project maintainers to review your pull request and provide feedback. ## Code of Conduct Please note that by contributing to the PresenceLight project, you are expected to adhere to the project's Code of Conduct. The CoC is simple, be respectful and considerate towards others in all interactions. ## License PresenceLight is licensed under the [MIT License](https://github.com/isaacrlevin/presencelight/blob/main/LICENSE). By contributing to this project, you agree to license your contributions under the same license. --- We appreciate your contributions to the PresenceLight project! Thank you for helping us make it better. ================================================ FILE: docs/configure-custom-api.md ================================================ # Custom API The Custom API page lets you use any generic service which has a web API which accepts GET or POST requests. For example, IFTTT webhooks can be used to run an action on any IFTTT-integrated service. In this way IFTTT can act as a bridge to other light services (such as Magic Home / MagicHue) or any other service which you may want to control with your Teams presence, e.g. 'When I'm in do not disturb pause Roomba'. To connect PresenceLight to a custom API: Configure the web service (e.g. created the applets in IFTTT) Enter the corresponding API method and URI against each presence state. ![Configured](../static/CustomAPI.png) The Custom API REST API calls also support providing a json formatted body to the endpoints (Uri) of Custom API. You can use the following variables in your JSON body: - {{availability}} - {{activity}} If you use above variables in the JSON body they will be replaced with the availability and/or activity values of your Microsoft Teams status. ## Home Assistant integration To use PresenceLight with Home Assistant you can use the Custom API functionality as follows: In Home Assistant you can use [Webhooks triggers](https://www.home-assistant.io/docs/automation/trigger/#webhook-trigger) to trigger an Automation Action, like turning on a light bulb. Example Automation for turning on a light bulb based on the Teams status send using the Custom API functionality of PresenceLight. ```yaml alias: Teams presence - IKEA Light Bulb Living Room description: >- Show the Microsoft Teams status via a color of the Light Bulb in the Living Room trigger: - platform: webhook allowed_methods: - POST local_only: true webhook_id: "" condition: [] action: - choose: - conditions: - condition: template value_template: "{{ trigger.json.presence_status == 'Busy' }}" sequence: - service: light.turn_on metadata: {} data: color_name: red target: entity_id: light.ikea_bulb - conditions: - condition: template value_template: "{{ trigger.json.presence_status == 'Available' }}" sequence: - service: light.turn_on metadata: {} data: color_name: green target: entity_id: light.ikea_bulb - conditions: - condition: template value_template: "{{ trigger.json.presence_status == 'Away' }}" sequence: - service: light.turn_on metadata: {} data: color_name: yellow target: entity_id: light.ikea_bulb - conditions: null sequence: - service: light.turn_off metadata: {} target: entity_id: light.ikea_bulb data: {} mode: single ``` In PresenceLight Custom API setting you need to enter the following information: | Method | Uri | Body | |--------|----------------|------| | POST | http://homeassistant.local:8123/api/webhook/webhook_id | { "presence_status":"Away" } | ================================================ FILE: docs/configure-entra-app.md ================================================ ## Configure an Entra ID Application 1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com/) using either a work or school account or a personal Microsoft account. 1. If your account gives you access to more than one tenant, select your account in the top right corner, and set your portal session to the desired Azure AD tenant (using **Switch Directory**). 1. In the left-hand navigation pane, select the **Entra ID** service, and then select **App registrations**. #### Register the client app (WpfApp) 1. Navigate to the Microsoft identity platform for developers [App registrations](https://go.microsoft.com/fwlink/?linkid=2083908) page. 1. Select **New registration**. - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `Presence Light`. - In the **Supported account types** section, select **Accounts in this organizational directory only (YOUR_TENANT_NAME only - Single tenant)**. - In the **Redirect URI (optional)** section, select **Public client/native (mobile & desktop)** and enter http://localhost for the value. - Select **Register** to create the application. 1. On the app **Overview** page, find the **Application (client) ID** value and record it for later. 1. On the app **Overview** page, find the **Directory (tenant) ID** value and record it for later.
![Ids](../static/id.png) 1. In the list of pages select **API permissions** - Select **Add a permission** - Ensure that the **Microsoft APIs** tab is selected - In the **Commonly used Microsoft APIs** section, click on **Microsoft Graph** - Select **Delegated permissions**. - Ensure that the right permissions are checked: **Presence.Read, User.Read**. Use the search box if necessary. See the screenshot below. - Select **Add permissions** - You can consent for your entire organization by selecting **Grant admin consent for YOUR_TENANT_NAME** - In the **Grant admin consent confirmation** section select **Yes** ![Api Permissions](..//static/api-perms.png) #### Configuring PresenceLight (Desktop) 1. Start `PresenceLight`. 1. Select **Settings** 1. Enter your **Directory (tenant) ID** or `common` if you elected to support Multitenant account types. 1. Enter your **Application (client) ID** 1. Select **SAVE SETTINGS** 1. Select **Team Status** 1. Select **SIGN IN** 1. Complete authentication in your browser. ================================================ FILE: docs/configure-hardware.md ================================================ ## Hue HW Notes ### Hue Hardware Requirements | Item | | ------------ | | [Philips Hue Bridge](https://www2.meethue.com/en-us/p/hue-bridge/046677458478) | [Philips Hue Light Bulb](https://www2.meethue.com/en-us/p/hue-white-and-color-ambiance-1-pack-e26/046677548483) | You will need the above Philips Hue items to broadcast your presence to, but you can still "use" PresenceLight without them. One of the requirements of the Bridge is that it needs to be hard-wired to an internet connection via ethernet, so it will need to be placed close to a router or network switch. There are steps to setup the bridge and bulb in the [Hardware and Connectivity Section](https://www2.meethue.com/en-us/support/hardware-and-connectivity) of the Philips Support Site, but you should be able to just plug the bridge, wait for the lights to light up, get the IP address for the bridge, enter it into the app, and register the device. The app will register your device, create an account to interact with the bulbs, and finally add any bulbs it finds. Philips also provides a Remote implementation of their connectivity (requires connecting your account to Philips Cloud). PresenceLight is configured to let you choose between the two. ## LIFX HW Notes ### LIFX Hardware Requirements [Any LIFX Light (tested with LIFX Beam & LIFX Color)](https://www.lifx.com/pages/all-products) LIFX Bulbs can be connected to over [LAN Protocol](https://lan.developer.lifx.com/), or [Cloud Api](https://lifx.readme.io/docs). PresenceLight uses the Cloud, which requires getting a token from the [developer portal](https://cloud.lifx.com/settings). Putting that token in PresenceLight will enable all connected lights. ## Philips Wiz | Item | | ------------ | | [Wiz Smart Bulb](https://www.wizconnected.com/en-us/products/bulbs) | PresenceLight uses LAN discovery to get all Philips Wiz smart bulbs on the network. ## Yeelight | Item | | ------------ | | [Yeelight Smart Bulb](https://store.yeelight.com/collections/smart-bulb) | PresenceLight uses LAN discovery to get all Yeelight smart bulbs on the network. ================================================ FILE: docs/desktop-README.md ================================================ ![Logo](https://github.com/isaacrlevin/PresenceLight/raw/main/Icon.png) # PresenceLight - Desktop Version ![.github/workflows/Deploy_Desktop.yml](https://github.com/isaacrlevin/presencelight/workflows/.github/workflows/Deploy_Desktop.yml/badge.svg) ## Desktop App Setup **NOTE: These steps are for the WPF (Windows desktop client) application. If you want to get PresenceLight working on non-Windows, please take a look at the [Web Readme](web-README.md).** ### Install App After you have followed installed the app, you will see a window like below ![Configured](../static/configured.png) PresenceLight obtains your Microsoft Teams Availability using a multi-tenant Microsoft Entra ID Application, meaning you will need to "grant" access to your Presence the first time you use the app. Clicking sign-in will prompt you for a login with your Microsoft 365 credentials, and finally when authenticated, you will be shown your Graph profile image and your presence. If you are curious about what is required to do this on your own tenant, read [Configure an Entra ID Application](configure-entra-app.md) ![Profile Image](../static/profile.png) The application "polls" the Presence Api at a configured value, which you can set between 1 and 5 seconds on the Settings page. This means that the light and app will update based on your Teams presence with a slight delay. ### Broadcasting to Lights There are 2 ways to currently update your lights using PresenceLight - Updating with Teams Presence (status) - Setting a fixed color using color picker You can only do one of these at a time, so if you for instance are syncing with Teams, choosing another option will sign you out of Teams. This will happen with the other options as well. ## Customize Icons One of the features of PresenceLight is that you can minimize the app to the icon tray. When you open the app, you will see an icon similar to this. ![white Image](../static/light-icon.png) This icon will represent your presence color. There are two "kinds" of icons: Transparent, and White. Here is the transparent icon ![Settings 1](../static/trans-icon.png) You can change the icon type in the settings pane. ![Settings 2](../static/settings1.png) After you change and save, the icon will update in the icon tray. ## Wire Up Philips Light To connect PresenceLight to Philips Hue, you can do it 1 of 2 ways - Obtain the IP Address of your Philips Hue Bridge (if you have it) - Ask PresenceLight to find it for you (may no work in certain network configurations) ![Hue Settings](../static/hue-settings.png) Once you have the IP of the bridge, you will need to register a developer account and get an Api Key. This is easily done by clicking the "Register Bridge" button. Clicking the button will popup a window asking you to press the sync button on the bridge, this is needed to register PresenceLight to the bridge. ![Sync Button](../static/sync-button.png) When PresenceLight is configured, you will see a dropdown of Hue Bulbs connected to the bridge for you to set your presence to. ![Registered Bridge](../static/registered-bridge.png) ## Wire up LIFX To connect PresenceLight to LIFX colored bulbs, you need to obtain a LIFX Developer Token. When you first arrive at the LIFX tab, you will see a message like this if you try to get Lights or Groups ![LIFX Unconfigured](../static/lifx-unconfigured.png) After entering an obtained token, you will be able to get a list of either individual lights or groups of lights, selecting one of the options and saving gives you a message like this ![LIFX Configured](../static/lifx-configured.png) ## [Wire-up Custom API](configure-custom-api.md) ## In Conclusion At this point PresenceLight should be setup. Feel free to file an issue if you have any problems. ================================================ FILE: docs/faq.md ================================================ ### What is the best version to install? It really depends on your workflow, for normal users, I would use the Microsoft Store as it least barrier to entry. ### How do I use my lights after installing PresenceLight? PresenceLight "polls" Graph and Windows Theme data until you tell it not to. The easiest way to do this is either shutdown the app, or do a one-time sync to a custom color, which should stop any polling. ### Where is X light? I would love PresenceLight to support EVERY smart light on the market, but I do not have the hardware to do that, I simply wrote a tool with the HW I have. If you want to add your own Smart Light brand, a PR is the fastest way. If you do, please follow the [Contributors Guide](CONTRIBUTING.md) ### This only runs on Windows, lame.... I am currently working on a cross-platform version using ASP.NET Core Blazor and Workers. I have been testing it on WSL2 as well as a RaspberryPi I have at home and it seems to be working well. I will release more info about that [here](web-README.md) ================================================ FILE: docs/web-README.md ================================================ ![Logo](Icon.png) # PresenceLight - Web Version ![.github/workflows/Deploy_Web.yml](https://github.com/isaacrlevin/presencelight/workflows/.github/workflows/Deploy_Web.yml/badge.svg) The cross platform version of PresenceLight runs as a .NET 9 single file executable application that runs a Server-Side Blazor Web Application and a ASP.NET Core Worker Service. The Blazor App is used as the mechanism to log the user in and configure settings of the app, while the Worker Service is responsible for interaction with Graph Api as well as the Smart Lights. This allows users to not need to have a UI version of the app open at all time, since the worker runs as a process. ## App Setup ### Prerequisites For PresenceLight to run out of the box, you need to setup a local SSL Cert for the app to run under. Here are two ways to do this - dotnet dev-certs - dotnet dev-certs https -ep C:\Users\youruserid\.aspnet\https\presencelight.pfx -p presencelight - dotnet dev-certs https --trust - openssl (Linux) - [Go here make your life easier](https://www.digicert.com/easy-csr/openssl.htm) - openssl x509 -signkey my_web_domain.key -in my_web_domain.csr -req -days 365 -out my_web_domain.crt - openssl pkcs12 -inkey my_web_domain.key -in my_web_domain.crt -export -out %PATHTOYOURCERT%/presencelight.pfx ### Install There is no installer for PresenceLight, so all that needs to be done is to download the zip folder from the [install site](http://presencelightapp.azurewebsites.net/), unzip, and run the .exe. At this point, a terminal window will open showing ![Terminal](../static/blazor-terminal.png) Here you will the Url for the Kestrel hosted Web Application, which will be `https://localhost:5001`. Going to that Url will take you through the login process for Azure Active Directory (for the Graph call). After login, you will see a similar look and feel to the client app. ![Index](../static/blazor-index.png) From here you can use PresenceLight in a similar way to the client app. You can enable and operate lights, push custom lights and configure polling. When done, you can close the browser and PresenceLight will continue to run in the background. To make the process even cleaner, you can configure a startup task to run the exe at startup, and PresenceLight will be available at the url listed the first time you ran it. ## Running PresenceLight as a container PresenceLight can be configured to run in a Docker container, and I have images on my [DockerHub](https://hub.docker.com/repository/docker/isaaclevin/presencelight) for the primary Linux distros. - x64 Linux (latest tag) - ARM64 (debian-arm64 tag) - ARM32 (debian-arm32 tag) **This is the 4GB Raspberry Pi one** ### How are you handling SSL? In order for PresenceLight to work, you need to have a redirect url to AAD that is https. In order to make it easy for folks, I provided a self-signed cert that will allow PresenceLight to do Https redirection out of the box. Isaac is this secure? Weeeeeeelllll not the best, but since PresenceLight runs locally you should be fine. If you want to expose PresenceLight over the internet, it more than likely won't work as I have to register EACH redirect uri with Azure AD. For my particular use-case I do not need SSL. WHAT?!?! Actually it is pretty cool. My personal setup is that PresenceLight runs in a docker container on a Raspberry Pi. I have Traefik, which is a well-known reverse proxy that allows me to forward applications through my domain, so I can access the application from anywhere by going to presencelight.mydomain.com The best part about this is that [Traefik](https://traefik.io/) can be configured to pull LetsEncrypt Certificates and integration with CloudFlare SSL. There is a [great blog post on this](https://www.smarthomebeginner.com/traefik-2-docker-tutorial/), that I highly reccomend if you are interested. ### SSL for Docker Containers To get PresenceLight to work in a Docker container, you will need to obtain (or generate like above) a certificate and mount it as a volume to your container. **[Doc on subject](https://docs.microsoft.com/dotnet/core/additional-tools/self-signed-certificates-guide)** Once you have a valid .pfx file, you will need to wire up the app to use that cert, the way you do that depends on how you host your app. If you app is just running locally on the machine, you can just set environment variables for your app. - ASPNETCORE_Kestrel__Certificates__Default__Path - ASPNETCORE_Kestrel__Certificates__Default__Password Or if you are running in docker, you will need to mount a volume that has your cert in it. Here are some examples for this **docker run example** ```bash docker run --rm -it -p 5000:80 -p 5001:443 -e ASPNETCORE_URLS="https://+;http://+" -e ASPNETCORE_HTTPS_PORT=5001 -e ASPNETCORE_Kestrel__Certificates__Default__Password="presencelight" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/presencelight.pfx -v $env:USERPROFILE\.aspnet\https:/https/ isaaclevin/presencelight ``` **docker-compose example** ```bash ports: - 5000:80 - 5001:443 volumes: C:\Users\isaac\.aspnet\https:/https/ #Windows Way /mnt/c/Users/isaac/.aspnet/https:/https #Linux Way environment: ASPNETCORE_HTTPS_PORT: "5001" ASPNETCORE_URLS: "https://+;http://+" ASPNETCORE_Kestrel__Certificates__Default__Password: "presencelight" ASPNETCORE_Kestrel__Certificates__Default__Path: "/https/presencelight.pfx" ``` ### Mounting settings path to host If you want to configure PresenceLight to use your own settings (maybe your own AAD, your own smart light registered app), you can do that by editing the appsettings.json To do this in docker, just run the container once, and than stop and rerun by mounting the appsettings via a local volume.** Log data and Configuration file will need to be written to a directory that has read/write enabled. This is accomplished using volumes. ```dotnetcli volumes: /somedirectory:/app/config ``` `/app/config/appsettings.json` contains settings for AAD and `/app/config/PresenceLightSettings.json` contains settings for Lights, in case you wanted to configure lights outside of the UI. If you need to customize your configuration. Add/edit one or more of the nec`essary configuration files in this attached directory. This will get you host access to the appsettings.json and PresenceLightSettings.json When running under a container, logs will save to `/app/config/logs` as well. ================================================ FILE: src/.editorconfig ================================================ # EditorConfig is awesome:http://EditorConfig.org # From https://raw.githubusercontent.com/dotnet/roslyn/master/.editorconfig # top-most EditorConfig file root = true # Don't use tabs for indentation. [*] indent_style = space trim_trailing_whitespace = true # (Please don't specify an indent_size here; that has too many unintended consequences.) # Code files [*.{cs,csx,vb,vbx}] indent_size = 4 insert_final_newline = true charset = utf-8-bom # Xml project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] indent_size = 2 # Xml config files [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] indent_size = 2 # Yml/Yaml files [*.{yaml,yml}] indent_size = 2 # Powershell files [*.ps1] indent_size = 2 # JSON files [*.json] indent_size = 2 # Shell scripts [*.sh] end_of_line = lf [*.{cmd,bat}] end_of_line = crlf # Dotnet code style settings: [*.{cs,vb}] # Sort using and Import directives with System.* appearing first dotnet_sort_system_directives_first = true # Put a blank line between System.* and Microsoft.* dotnet_separate_import_directive_groups = true # Avoid "this." and "Me." if not necessary dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_property = false:suggestion dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_event = false:suggestion # Use language keywords instead of framework type names for type references dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion # Prefer read-only on fields dotnet_style_readonly_field = false:warning # Suggest more modern language features when available dotnet_style_object_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion dotnet_style_prefer_conditional_expression_over_return = false dotnet_style_prefer_conditional_expression_over_assignment = false dotnet_style_prefer_auto_properties = true:suggestion # Parentheses dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent # Accessibility modifiers dotnet_style_require_accessibility_modifiers = always:suggestion # Naming Rules # Interfaces start with an I and are PascalCased dotnet_naming_rule.interfaces_must_be_pascal_cased_and_prefixed_with_I.symbols = interface_symbols dotnet_naming_rule.interfaces_must_be_pascal_cased_and_prefixed_with_I.style = pascal_case_and_prefix_with_I_style dotnet_naming_rule.interfaces_must_be_pascal_cased_and_prefixed_with_I.severity = warning # External members are PascalCased dotnet_naming_rule.externally_visible_members_must_be_pascal_cased.symbols = externally_visible_symbols dotnet_naming_rule.externally_visible_members_must_be_pascal_cased.style = pascal_case_style dotnet_naming_rule.externally_visible_members_must_be_pascal_cased.severity = warning # Parameters are camelCased dotnet_naming_rule.parameters_must_be_camel_cased.symbols = parameter_symbols dotnet_naming_rule.parameters_must_be_camel_cased.style = camel_case_style dotnet_naming_rule.parameters_must_be_camel_cased.severity = warning # Constants are PascalCased dotnet_naming_rule.constants_must_be_pascal_cased.symbols = constant_symbols dotnet_naming_rule.constants_must_be_pascal_cased.style = pascal_case_style dotnet_naming_rule.constants_must_be_pascal_cased.severity = warning # Uncomment this group and comment out the next group if you prefer s_ prefixes for static fields # Private static fields are prefixed with s_ and are camelCased like s_myStatic #dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.symbols = private_static_field_symbols #dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.style = camel_case_and_prefix_with_s_underscore_style #dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.severity = warning # Static readonly fields are PascalCased dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.symbols = private_static_readonly_field_symbols dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.severity = warning # Comment this group and uncomment out the next group if you don't want _ prefixed fields. # Private instance fields are camelCased with an _ like _myField dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.symbols = private_field_symbols dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.style = camel_case_and_prefix_with_underscore_style dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.severity = warning # Private instance fields are camelCased #dotnet_naming_rule.private_instance_fields_must_be_camel_cased.symbols = private_field_symbols #dotnet_naming_rule.private_instance_fields_must_be_camel_cased.style = camel_case_style #dotnet_naming_rule.private_instance_fields_must_be_camel_cased.severity = warning # Symbols dotnet_naming_symbols.externally_visible_symbols.applicable_kinds = class,struct,interface,enum,property,method,field,event,delegate dotnet_naming_symbols.externally_visible_symbols.applicable_accessibilities = public,internal,friend,protected,protected_internal,protected_friend,private_protected dotnet_naming_symbols.interface_symbols.applicable_kinds = interface dotnet_naming_symbols.interface_symbols.applicable_accessibilities = * dotnet_naming_symbols.parameter_symbols.applicable_kinds = parameter dotnet_naming_symbols.parameter_symbols.applicable_accessibilities = * dotnet_naming_symbols.constant_symbols.applicable_kinds = field dotnet_naming_symbols.constant_symbols.required_modifiers = const dotnet_naming_symbols.constant_symbols.applicable_accessibilities = * dotnet_naming_symbols.private_static_field_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_field_symbols.required_modifiers = static,shared dotnet_naming_symbols.private_static_field_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly_field_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_readonly_field_symbols.required_modifiers = static,shared,readonly dotnet_naming_symbols.private_static_readonly_field_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_field_symbols.applicable_kinds = field dotnet_naming_symbols.private_field_symbols.applicable_accessibilities = private # Styles dotnet_naming_style.camel_case_style.capitalization = camel_case dotnet_naming_style.pascal_case_style.capitalization = pascal_case dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.required_prefix = s_ dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.capitalization = camel_case dotnet_naming_style.camel_case_and_prefix_with_underscore_style.required_prefix = _ dotnet_naming_style.camel_case_and_prefix_with_underscore_style.capitalization = camel_case dotnet_naming_style.pascal_case_and_prefix_with_I_style.required_prefix = I dotnet_naming_style.pascal_case_and_prefix_with_I_style.capitalization = pascal_case # CSharp code style settings: [*.cs] # Modifier order csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion # Code block csharp_prefer_braces = false:none # Indentation preferences csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true csharp_indent_switch_labels = true csharp_indent_labels = flush_left # Prefer "var" everywhere csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_elsewhere = true:suggestion # Code style defaults csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = true # Prefer method-like constructs to have a block body csharp_style_expression_bodied_methods = false:none csharp_style_expression_bodied_constructors = false:none csharp_style_expression_bodied_operators = false:none # Prefer property-like constructs to have an expression-body csharp_style_expression_bodied_properties = true:none csharp_style_expression_bodied_indexers = true:none csharp_style_expression_bodied_accessors = true:none # Expression csharp_prefer_simple_default_expression = true:suggestion csharp_style_deconstructed_variable_declaration = true:suggestion csharp_style_pattern_local_over_anonymous_function = true:suggestion # Pattern matching csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion # Null checking preferences csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion # Newline settings csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true # Space preferences csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true csharp_space_after_comma = true csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after csharp_space_around_declaration_statements = do_not_ignore csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false csharp_space_before_open_square_brackets = false csharp_space_before_semicolon_in_for_statement = false csharp_space_between_empty_square_brackets = false csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false # CA1303: Do not pass literals as localized parameters dotnet_diagnostic.CA1303.severity = none # CA1051: Do not declare visible instance fields dotnet_diagnostic.CA1051.severity = none # CA1031: Do not catch general exception types dotnet_diagnostic.CA1031.severity = none # CA1812: Avoid uninstantiated internal classes dotnet_diagnostic.CA1812.severity = silent # CA1816: Dispose methods should call SuppressFinalize dotnet_diagnostic.CA1816.severity = silent # CA1054: Uri parameters should not be strings dotnet_diagnostic.CA1054.severity = none dotnet_diagnostic.CA1056.severity = none # Default severity for analyzer diagnostics with category 'Style' dotnet_analyzer_diagnostic.category-Style.severity = silent ================================================ FILE: src/DesktopClient/Directory.Build.props ================================================ Isaac Levin © 2023 Isaac Levin true en-US 1701;1702;1705;1591;NU1701 $(MSBuildProjectName.Equals('PresenceLight')) $(MSBuildProjectName.Contains('.Package')) embedded $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb true true true true $(MSBuildWarningsAsMessages);NETSDK1107 preview enable enable 5.8.0 Debug $(DefineConstants);NIGHTLY $(DefineConstants);RELEASE $(DefineConstants);STANDALONE ================================================ FILE: src/DesktopClient/Directory.Build.targets ================================================ <_Parameter1>CommitHash <_Parameter2>$(SourceRevisionId) <_Parameter1>CloudBuildNumber <_Parameter2>$(BuildVersionSimple) <_Parameter1>CloudBuildNumber <_Parameter2>$(BuildVersionSimple)$(SemVerBuildSuffix) ================================================ FILE: src/DesktopClient/PresenceLight/App.xaml ================================================  ================================================ FILE: src/DesktopClient/PresenceLight/App.xaml.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Windows; using Blazorise; using Blazorise.Bootstrap; using Blazorise.Icons.FontAwesome; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MudBlazor.Services; using PresenceLight.Core; using PresenceLight.Razor; using PresenceLight.Razor.Services; using PresenceLight.Services; using PresenceLight.Telemetry; using Serilog; using Windows.Storage; namespace PresenceLight { /// /// Interaction logic for App.xaml /// public partial class App : System.Windows.Application { public IServiceProvider? ServiceProvider { get; private set; } public IConfiguration? Configuration { get; private set; } public App() { } private void OnStartup(object sender, StartupEventArgs e) { if (SingleInstanceAppMutex.TakeExclusivity()) { Exit += (_, __) => SingleInstanceAppMutex.ReleaseExclusivity(); try { ContinueStartup(); } catch (Exception ex) when (IsCriticalFontLoadFailure(ex)) { Trace.WriteLine($"## Warning Notify ##: {ex}"); Log.Error(ex, "Stopped program because of exception"); } } else { Log.CloseAndFlush(); Shutdown(); } } Dictionary InMemorySettings = new(); private void ContinueStartup() { IServiceCollection services = new ServiceCollection(); // Configuration Section var builder = new Microsoft.Extensions.Configuration.ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.Development.json", optional: true, reloadOnChange: true); string userAppSettings; //Override the save file location for logs if this is a packaged app... if (new DesktopBridge.Helpers().IsRunningAsUwp()) { var _logFilePath = System.IO.Path.Combine(ApplicationData.Current.LocalFolder.Path, "PresenceLight\\logs\\DesktopClient\\log-.json"); InMemorySettings.Add("Serilog:WriteTo:1:Args:Path", _logFilePath); builder.AddInMemoryCollection(InMemorySettings); userAppSettings = AppPackageSettingsService.BuildSettingsFileLocation(); } else { userAppSettings = StandaloneSettingsService.BuildSettingsFileLocation(); } builder.AddJsonFile(userAppSettings, optional: true, reloadOnChange: true); Configuration = builder.Build(); services.Configure(Configuration); services.AddSingleton(Configuration); services.AddOptions(); services.Configure(Configuration.GetSection("AADSettings")); //Logging var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); telemetryConfiguration.InstrumentationKey = Configuration["ApplicationInsights:InstrumentationKey"]; var loggerConfig = new LoggerConfiguration() .ReadFrom.Configuration(Configuration) .WriteTo.PresenceEventsLogSink() .Enrich.FromLogContext(); Log.Logger = loggerConfig.CreateLogger(); Log.Debug("Starting PresenceLight"); services.AddLogging(logging => { logging.AddSerilog(); }); #if DEBUG services.AddBlazorWebViewDeveloperTools(); #endif services.Configure((o) => { o.InstrumentationKey = Configuration["ApplicationInsights:InstrumentationKey"]; o.TelemetryInitializers.Add(new OperationCorrelationTelemetryInitializer()); }); services.AddApplicationInsightsTelemetryWorkerService(options => { options.EnablePerformanceCounterCollectionModule = false; options.EnableDependencyTrackingTelemetryModule = false; }); //Blazor services.AddMudServices(); services.AddHttpClient(); services.AddHttpContextAccessor(); services.AddWpfBlazorWebView(); services.AddBlazorise(options => { options.Immediate = true; }) .AddBootstrapProviders() .AddFontAwesomeIcons(); services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(typeof(App).Assembly); cfg.RegisterServicesFromAssembly(typeof(BaseConfig).Assembly); }); //Singleton Services services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddPresenceServices(); services.AddSingleton(); services.AddSingleton(); services.AddTransient(); if (new DesktopBridge.Helpers().IsRunningAsUwp()) { services.AddSingleton(); } else { services.AddSingleton(); } services.AddSingleton(); //Inject Services Into MainWindow ServiceProvider = services.BuildServiceProvider(); var configuration = ServiceProvider.GetService(); if (configuration != null) { var b = configuration.DefaultTelemetrySink.TelemetryProcessorChainBuilder; double fixedSamplingPercentage = 10; b.UseSampling(fixedSamplingPercentage); b.Build(); } var mainWindow = ServiceProvider.GetRequiredService(); mainWindow.Show(); } private static bool IsCriticalFontLoadFailure(Exception ex) { return ex.StackTrace.Contains("MS.Internal.Text.TextInterface.FontFamily.GetFirstMatchingFont", StringComparison.OrdinalIgnoreCase) || ex.StackTrace.Contains("MS.Internal.Text.Line.Format", StringComparison.OrdinalIgnoreCase); } } } ================================================ FILE: src/DesktopClient/PresenceLight/MainWindow.xaml ================================================  ================================================ FILE: src/DesktopClient/PresenceLight/MainWindow.xaml.cs ================================================ using System; using System.IO; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Graph; using Microsoft.Graph.Models; using Microsoft.Identity.Client; using PresenceLight.Core; using PresenceLight.Telemetry; namespace PresenceLight { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { private readonly BaseConfig _options; // private Presence presence { get; set; } private DateTime settingsLastSaved = DateTime.MinValue; private MediatR.IMediator _mediator; private readonly LoginService _loginService; private DiagnosticsClient _diagClient; private ISettingsService _settingsService; private WindowState lastWindowState; private bool isInteractRunning; private readonly ILogger _logger; private readonly AppState _appState = new AppState(); #region Init public MainWindow(LoginService loginService, MediatR.IMediator mediator, IOptionsMonitor optionsAccessor, DiagnosticsClient diagClient, ILogger logger, ISettingsService settingsService, AppState appState) { var currentApp = (App)System.Windows.Application.Current; Resources.Add("services", currentApp.ServiceProvider); InitializeComponent(); _appState = appState; _logger = logger; System.Windows.Application.Current.SessionEnding += new SessionEndingCancelEventHandler(Current_SessionEnding); _loginService = loginService; _mediator = mediator; _options = optionsAccessor != null ? optionsAccessor.CurrentValue : throw new NullReferenceException("Options Accessor is null"); _diagClient = diagClient; _settingsService = settingsService; LoadSettings().ContinueWith( async t => { if (t.IsFaulted) { var foo = ""; } await Task.Run(async () => { this.Dispatcher.Invoke(() => { appState.SignedIn = false; LoadApp(); var tbContext = notificationIcon.DataContext; DataContext = _appState.Config; notificationIcon.DataContext = tbContext; if (_appState.Config.StartMinimized) { this.Hide(); } }); while (true) { await Task.Run(async () => { Thread.Sleep(100); if (_appState.SignInRequested) { _appState.SignInRequested = false; await this.Dispatcher.BeginInvoke(async () => { await SignIn(); }); } if (_appState.SignOutRequested) { _appState.SignOutRequested = false; await this.Dispatcher.BeginInvoke(async () => { await SignOut(); }); } if (_appState.RebuildRequested) { _appState.RebuildRequested = false; await this.Dispatcher.BeginInvoke(async () => { await RebuildClient(); }); } }); } }); }, TaskScheduler.Current); } private async Task LoadSettings() { try { _logger.LogInformation("Load Settings Initialized"); if (!(await _settingsService.IsFilePresent())) { await _settingsService.SaveSettings(_options); } _appState.SetConfig(await _settingsService.LoadSettings() ?? throw new NullReferenceException("Settings Load Service Returned null")); bool useWorkingHours = await _mediator.Send(new Core.WorkingHoursServices.UseWorkingHoursCommand()); bool IsInWorkingHours = await _mediator.Send(new Core.WorkingHoursServices.IsInWorkingHoursCommand()); _logger.LogInformation("Load Settings Successfull"); } catch (Exception e) { _logger.LogError(e, "Error occurred Loading Settings"); _diagClient.TrackException(e); } } private async void MainWindow_Loaded(object sender, RoutedEventArgs e) { await blazorWebView1.WebView.EnsureCoreWebView2Async(); blazorWebView1.WebView.CoreWebView2.Settings.IsZoomControlEnabled = false; } private void LoadApp() { try { notificationIcon.Text = $"PresenceLight Status - {PresenceConstants.Inactive}"; notificationIcon.Icon = new BitmapImage(new Uri(IconConstants.GetIcon(string.Empty, string.Empty))); _appState.Config.LightSettings.WorkingHoursStartTimeAsDate = string.IsNullOrEmpty(_appState.Config.LightSettings.WorkingHoursStartTime) ? null : DateTime.Parse(_appState.Config.LightSettings.WorkingHoursStartTime, null); _appState.Config.LightSettings.WorkingHoursEndTimeAsDate = string.IsNullOrEmpty(_appState.Config.LightSettings.WorkingHoursEndTime) ? null : DateTime.Parse(_appState.Config.LightSettings.WorkingHoursEndTime, null); CallGraph(); } catch (Exception e) { _logger.LogError(e, $"Error occurred - {e.Message}"); } } #endregion #region Profile Panel private async Task SignIn() { await CallGraph(); } private async Task CallGraph() { _appState.SetLightMode("Graph"); _logger.LogInformation("Light Mode Set: Graph"); if (!await _mediator.Send(new Core.GraphServices.GetIsInitializedCommand())) { await _mediator.Send(new Core.GraphServices.InitializeCommand() { }); if (_loginService.IsInitialized) { _appState.SignedIn = true; } } try { await _settingsService.SaveSettings(_appState.Config); if (!isInteractRunning) { await InteractWithLights(); } } catch (Exception e) { _logger.LogError(e, "Error occurred calling Graph"); } } public async Task SetColor(string color, string activity = "") { try { if (_appState.Config.LightSettings.Hue.IsEnabled) { if (Helpers.AreStringsNotEmpty(new string[] { _appState.Config.LightSettings.Hue.HueApiKey, _appState.Config.LightSettings.Hue.SelectedItemId})) { if (_appState.Config.LightSettings.Hue.UseRemoteApi) { if (!string.IsNullOrEmpty(_appState.Config.LightSettings.Hue.RemoteBridgeId)) { await _mediator.Send(new Core.RemoteHueServices.SetColorCommand { Availability = color, Activity = activity, LightId = _appState.Config.LightSettings.Hue.SelectedItemId, BridgeId = _appState.Config.LightSettings.Hue.RemoteBridgeId }); } } if (!string.IsNullOrEmpty(_appState.Config.LightSettings.Hue.HueIpAddress)) { await _mediator.Send(new Core.HueServices.SetColorCommand() { Activity = activity, Availability = color, LightID = _appState.Config.LightSettings.Hue.SelectedItemId }); } } } if (_appState.Config.LightSettings.LIFX.IsEnabled && !string.IsNullOrEmpty(_appState.Config.LightSettings.LIFX.LIFXApiKey)) { await _mediator.Send(new PresenceLight.Core.LifxServices.SetColorCommand { Activity = activity, Availability = color, LightId = _appState.Config.LightSettings.LIFX.SelectedItemId }); } if (_appState.Config.LightSettings.Wiz.IsEnabled) { await _mediator.Send(new PresenceLight.Core.WizServices.SetColorCommand { Activity = activity, Availability = color, LightID = _appState.Config.LightSettings.Wiz.SelectedItemId }); } if (_appState.Config.LightSettings.Yeelight.IsEnabled && !string.IsNullOrEmpty(_appState.Config.LightSettings.Yeelight.SelectedItemId)) { await _mediator.Send(new PresenceLight.Core.YeelightServices.SetColorCommand { Activity = activity, Availability = color, LightId = _appState.Config.LightSettings.Yeelight.SelectedItemId }); } if (_appState.Config.LightSettings.CustomApi.IsEnabled) { string response = await _mediator.Send(new Core.CustomApiServices.SetColorCommand() { Activity = activity, Availability = color }); } if (_appState.Config.LightSettings.LocalSerialHost.IsEnabled) { string response = await _mediator.Send(new Core.LocalSerialHostServices.SetColorCommand() { Activity = activity, Availability = color }); } } catch (Exception e) { _logger.LogError(e, "Error occurred Setting Color"); } } private async Task SignOut() { _logger.LogInformation("Signing out of Graph PresenceLight Sync"); _appState.SetLightMode("Graph"); try { await _loginService.SignOut(); _appState.SignedIn = false; _appState.SetUserInfo(null, null, null); notificationIcon.Text = $"PresenceLight Status - {PresenceConstants.Inactive}"; notificationIcon.Icon = new BitmapImage(new Uri(IconConstants.GetIcon(string.Empty, string.Empty))); await SetColor("Off"); } catch (MsalException) { } await _settingsService.SaveSettings(_appState.Config); } private async Task RebuildClient() { _appState.RebuildRequested = false; await SignOut(); _loginService.RebuildClient(); //this was called by signout before, but reusing it here to trigger NotifyStateChanged _appState.SetUserInfo(null, null, null); } #endregion #region UI Helpers private BitmapImage? LoadImage(byte[] imageData) { try { if (imageData == null || imageData.Length == 0) return null; var image = new BitmapImage(); using (var mem = new MemoryStream(imageData)) { mem.Position = 0; image.BeginInit(); image.CreateOptions = BitmapCreateOptions.PreservePixelFormat; image.CacheOption = BitmapCacheOption.OnLoad; image.UriSource = null; image.StreamSource = mem; image.EndInit(); } image.Freeze(); return image; } catch (Exception e) { _logger.LogError(e, "Error occurred in LoadImager"); throw; } } public void MapUI(Presence presence) { try { SolidColorBrush mySolidColorBrush = new SolidColorBrush(); if (presence != null) { notificationIcon.Text = $"PresenceLight Status - {Helpers.HumanifyText(presence.Availability)}"; notificationIcon.Icon = new BitmapImage(new Uri(IconConstants.GetIcon(_appState.Config.IconType, presence.Availability))); } } catch (Exception e) { _logger.LogError(e, "Error Occurred Mapping UI"); throw; } } #endregion #region Graph Calls public async Task GetPresence() { try { return await _mediator.Send(new Core.GraphServices.GetPresenceCommand()); } catch (Exception e) { _logger.LogError(e, "Error occurred Getting Presence"); throw; } } public async Task GetPhoto() { try { var photo = await _mediator.Send(new Core.GraphServices.GetPhotoCommand()); if (photo == null) { return null; } else { return StreamToByteArray(photo); } } catch (Exception e) { _logger.LogError(e, "Error occurred Getting Photo"); return null; } } public static byte[] StreamToByteArray(Stream input) { byte[] buffer = new byte[16 * 1024]; using (MemoryStream ms = new MemoryStream()) { int read; while ((read = input.Read(buffer, 0, buffer.Length)) > 0) { ms.Write(buffer, 0, read); } return ms.ToArray(); } } #endregion #region Tray Methods protected override async void OnClosing(System.ComponentModel.CancelEventArgs e) { e.Cancel = true; await _settingsService.SaveSettings(_appState.Config); this.Hide(); } private void OnNotifyIconDoubleClick(object sender, MouseButtonEventArgs e) { if (e.ChangedButton == MouseButton.Left) { this.Show(); this.WindowState = this.lastWindowState; } } private void OnOpenClick(object sender, RoutedEventArgs e) { this.Show(); this.WindowState = this.lastWindowState; } private void OnTurnOnSyncClick(object sender, RoutedEventArgs e) { _appState.SetLightMode("Graph"); this.WindowState = this.lastWindowState; _logger.LogInformation("Turning On PresenceLight Sync"); } private async void OnTurnOffSyncClick(object sender, RoutedEventArgs e) { try { _appState.SetLightMode("Custom"); await SetColor("Off", "Off"); notificationIcon.Text = PresenceConstants.Inactive; notificationIcon.Icon = new BitmapImage(new Uri(IconConstants.GetIcon(string.Empty, string.Empty))); this.WindowState = this.lastWindowState; } catch (Exception ex) { _logger.LogError(ex, "Error occurred turning Off Sync"); } _logger.LogInformation("Turning Off PresenceLight Sync"); } private async void OnExitClick(object sender, RoutedEventArgs e) { try { await SetColor("Off", "Off"); await _settingsService.SaveSettings(_appState.Config); System.Windows.Application.Current.Shutdown(); } catch (Exception ex) { _logger.LogError(ex, "Error occurred Exiting"); } _logger.LogInformation("PresenceLight Exiting"); } private async void Current_SessionEnding(object sender, SessionEndingCancelEventArgs e) { try { await SetColor("Off", "Off"); await _settingsService.SaveSettings(_appState.Config); } catch (Exception ex) { _logger.LogError(ex, "Error occurred Ending Session"); } _logger.LogInformation("PresenceLight Session Ending"); } #endregion private async Task InteractWithLights() { bool previousWorkingHours = false; string previousLightMode = string.Empty; while (true) { isInteractRunning = true; try { if (_appState.SignedIn) { if (_appState.User == null || string.IsNullOrEmpty(_appState.User.DisplayName)) { try { var (profile, presence) = await _mediator.Send(new Core.GraphServices.GetProfileAndPresenceCommand()); var photo = await GetPhoto(); _appState.SetLightMode("Graph"); if (photo == null) { MapUI(presence); _appState.SetUserInfo(profile, presence); } else { MapUI(presence); _appState.SetUserInfo(profile, presence, $"data:image/gif;base64,{Convert.ToBase64String(photo)}"); } } catch (ServiceException ex) { if (ex.ResponseStatusCode == (int) System.Net.HttpStatusCode.Unauthorized || ex.ResponseStatusCode == (int) System.Net.HttpStatusCode.Forbidden) { _logger.LogWarning("Error getting profile and presence info. Something is likely corrupt. Requesting sign out."); _appState.SignOutRequested = true; } } } await Task.Delay(Convert.ToInt32(_appState.Config.LightSettings.PollingInterval * 1000)); bool touchLight = false; string newColor = ""; if (_appState.Config.LightSettings.SyncLights) { if (!await _mediator.Send(new Core.WorkingHoursServices.UseWorkingHoursCommand())) { if (_appState.LightMode == "Graph") { touchLight = true; } } else { var isInWorkingHours = await _mediator.Send(new Core.WorkingHoursServices.IsInWorkingHoursCommand()); if (isInWorkingHours) { previousWorkingHours = isInWorkingHours; if (_appState.LightMode == "Graph") { touchLight = true; } } else { // check to see if working hours have passed if (previousWorkingHours) { previousWorkingHours = false; previousLightMode = _appState.LightMode; switch (_appState.Config.LightSettings.HoursPassedStatus) { case "White": newColor = "Offline"; _appState.SetLightMode("Manual"); break; case "Off": newColor = "Off"; _appState.SetLightMode("Manual"); break; case "Keep": default: break; } touchLight = true; } } } } if (touchLight && _appState.SignedIn) { switch (_appState.LightMode) { case "Manual": // No need to check presence... if it's after hours, we just want to action upon it... await SetColor(newColor, _appState.Presence.Activity); //Reset the light mode so that we don't potentially mess something up. _appState.SetLightMode(previousLightMode); break; case "Graph": _logger.LogInformation("PresenceLight Running in Teams Mode"); _appState.SetPresence(await System.Threading.Tasks.Task.Run(() => GetPresence())); if (newColor == string.Empty) { await SetColor(_appState.Presence.Availability, _appState.Presence.Activity); } else { await SetColor(newColor, _appState.Presence.Activity); } if (DateTime.Now.AddMinutes(-5) > settingsLastSaved) { await _settingsService.SaveSettings(_appState.Config); settingsLastSaved = DateTime.Now; } MapUI(_appState.Presence); break; default: break; } } } else { isInteractRunning = false; break; } } catch (Exception e) { _logger.LogError(e, "Error occurred interacting with lights"); } } } } } ================================================ FILE: src/DesktopClient/PresenceLight/PresenceLight.csproj ================================================  net10.0-windows10.0.19041 WinExe PresenceLight PresenceLight PresenceLight is a solution to broadcast your Microsoft Teams presence to a Philips Hue or LIFX light bulb. There are other solutions that do something similar, but they require a tethered solution (plugging a light into a computer via USB). What PresenceLight does is leverage the Presence Api, which is available in Microsoft Graph, allowing to retrieve your presence without having to be tethered. This could potentially allow someone to update the light bulb from a remote machine they do not use. PresenceLight true true Properties\app.manifest true Icons\Icon.ico Always Always Always Always Always Always Always Always Always Always Always Always Always Always Always Always Always Always Always Always Always Always Always Always ================================================ FILE: src/DesktopClient/PresenceLight/PresenceLight.exe.gui ================================================ ================================================ FILE: src/DesktopClient/PresenceLight/Properties/AssemblyInfo.cs ================================================ using System.Windows; [assembly:ThemeInfo( ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located //(used if a resource is not found in the page, // or application resource dictionaries) ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located //(used if a resource is not found in the page, // app, or any theme specific resource dictionaries) )] ================================================ FILE: src/DesktopClient/PresenceLight/Properties/PublishProfiles/WinARM64.pubxml ================================================  FileSystem ARM64 net10.0-windows10.0.19041 win-arm64 bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ true False False False ================================================ FILE: src/DesktopClient/PresenceLight/Properties/PublishProfiles/WinX64.pubxml ================================================  FileSystem x64 net10.0-windows10.0.19041 win-x64 bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ true False False False ================================================ FILE: src/DesktopClient/PresenceLight/Properties/PublishProfiles/WinX86.pubxml ================================================ FileSystem x86 net10.0-windows10.0.19041 win-x86 bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ true False False False ================================================ FILE: src/DesktopClient/PresenceLight/Properties/Resources.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace PresenceLight.Properties { using System; /// /// A strongly-typed resource class, for looking up localized strings, etc. /// // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PresenceLight.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// internal static System.Drawing.Icon Available { get { object obj = ResourceManager.GetObject("Available", resourceCulture); return ((System.Drawing.Icon)(obj)); } } /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// internal static System.Drawing.Icon Away { get { object obj = ResourceManager.GetObject("Away", resourceCulture); return ((System.Drawing.Icon)(obj)); } } /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// internal static System.Drawing.Icon BeRightBack { get { object obj = ResourceManager.GetObject("BeRightBack", resourceCulture); return ((System.Drawing.Icon)(obj)); } } /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// internal static System.Drawing.Icon Busy { get { object obj = ResourceManager.GetObject("Busy", resourceCulture); return ((System.Drawing.Icon)(obj)); } } /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// internal static System.Drawing.Icon DoNotDisturb { get { object obj = ResourceManager.GetObject("DoNotDisturb", resourceCulture); return ((System.Drawing.Icon)(obj)); } } /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// internal static System.Drawing.Icon Icon { get { object obj = ResourceManager.GetObject("Icon", resourceCulture); return ((System.Drawing.Icon)(obj)); } } /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// internal static System.Drawing.Icon Inactive { get { object obj = ResourceManager.GetObject("Inactive", resourceCulture); return ((System.Drawing.Icon)(obj)); } } } } ================================================ FILE: src/DesktopClient/PresenceLight/Properties/Resources.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ..\Icons\Available.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Icons\Away.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Icons\BeRightBack.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Icons\Busy.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Icons\DoNotDisturb.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Icons\Icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Icons\Inactive.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ================================================ FILE: src/DesktopClient/PresenceLight/Properties/Settings.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace PresenceLight.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); public static Settings Default { get { return defaultInstance; } } } } ================================================ FILE: src/DesktopClient/PresenceLight/Properties/Settings.settings ================================================  ================================================ FILE: src/DesktopClient/PresenceLight/Properties/app.manifest ================================================  true/PM PerMonitorV2, PerMonitor ================================================ FILE: src/DesktopClient/PresenceLight/Services/Constants.cs ================================================  using System; using System.Reflection; namespace PresenceLight { public static class PresenceConstants { public const string Inactive = "Not Logged In"; } public class PresenceColors { public const string Available = "#009933"; public const string AvailableIdle = "#FFFF00"; public const string Busy = "#FF3300"; public const string BusyIdle = "#FFFF00"; public const string BeRightBack = "#FFFF00"; public const string Away = "#FFFF00"; public const string DoNotDisturb = "#B03CDE"; public const string OutOfOffice = "#800080"; public const string Offline = "#FFFFFF"; public const string Inactive = "#FFFFFF"; public static string GetColor(string status) { var pc = new PresenceColors(); Type type =pc.GetType(); PropertyInfo[] props = type.GetProperties(); foreach (var prop in props) { if (prop.Name == status) { return prop.GetValue(pc).ToString(); } } return PresenceColors.Inactive; } } public static class IconConstants { private static string Base = "pack://application:,,,/PresenceLight;component/icons/"; public static string GetIcon(string iconType, string status) { if (string.IsNullOrEmpty(status)) { status = "Inactive"; } if (iconType == "Transparent") { return $"{Base}t_{status}.ico"; } else { return $"{Base}{status}.ico"; } } } } ================================================ FILE: src/DesktopClient/PresenceLight/Services/MessageBoxHelper.cs ================================================ using System; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; namespace PresenceLight { internal static class MessageBoxHelper { internal static void PrepToCenterMessageBoxOnForm(Window form) { MessageBoxCenterHelper helper = new MessageBoxCenterHelper(); helper.Prep(form); } private class MessageBoxCenterHelper { private int messageHook; private IntPtr parentFormHandle; public void Prep(Window form) { NativeMethods.CenterMessageCallBackDelegate callBackDelegate = new NativeMethods.CenterMessageCallBackDelegate(CenterMessageCallBack); GCHandle.Alloc(callBackDelegate); parentFormHandle = new WindowInteropHelper(form).Handle; messageHook = NativeMethods.SetWindowsHookEx(5, callBackDelegate, new IntPtr(NativeMethods.GetWindowLong(parentFormHandle, -6)), NativeMethods.GetCurrentThreadId()).ToInt32(); } private int CenterMessageCallBack(int message, int wParam, int lParam) { NativeMethods.RECT formRect; NativeMethods.RECT messageBoxRect; int xPos; int yPos; if (message == 5) { NativeMethods.GetWindowRect(parentFormHandle, out formRect); NativeMethods.GetWindowRect(new IntPtr(wParam), out messageBoxRect); xPos = (int)((formRect.Left + (formRect.Right - formRect.Left) / 2) - ((messageBoxRect.Right - messageBoxRect.Left) / 2)); yPos = (int)((formRect.Top + (formRect.Bottom - formRect.Top) / 2) - ((messageBoxRect.Bottom - messageBoxRect.Top) / 2)); NativeMethods.SetWindowPos(wParam, 0, xPos, yPos, 0, 0, 0x1 | 0x4 | 0x10); NativeMethods.UnhookWindowsHookEx(messageHook); } return 0; } } private static class NativeMethods { internal struct RECT { public int Left; public int Top; public int Right; public int Bottom; } internal delegate int CenterMessageCallBackDelegate(int message, int wParam, int lParam); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool UnhookWindowsHookEx(int hhk); [DllImport("user32.dll", SetLastError = true)] internal static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("kernel32.dll")] internal static extern int GetCurrentThreadId(); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr SetWindowsHookEx(int hook, CenterMessageCallBackDelegate callback, IntPtr hMod, int dwThreadId); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SetWindowPos(int hWnd, int hWndInsertAfter, int X, int Y, int cx, int cy, int uFlags); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); } } } ================================================ FILE: src/DesktopClient/PresenceLight/Services/NotifyIcon.cs ================================================ using System; using System.ComponentModel; using System.Windows; using System.Windows.Input; using System.Windows.Markup; using System.Windows.Media; using Drawing = System.Drawing; using Forms = System.Windows.Forms; namespace PresenceLight { [ContentProperty("Text")] [DefaultEvent("MouseDoubleClick")] public partial class NotifyIcon : FrameworkElement, IAddChild { public static readonly RoutedEvent MouseClickEvent = EventManager.RegisterRoutedEvent( "MouseClick", RoutingStrategy.Bubble, typeof(MouseButtonEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent MouseDoubleClickEvent = EventManager.RegisterRoutedEvent( "MouseDoubleClick", RoutingStrategy.Bubble, typeof(MouseButtonEventHandler), typeof(NotifyIcon)); public static readonly DependencyProperty IconProperty = DependencyProperty.Register( "Icon", typeof(ImageSource), typeof(NotifyIcon), new FrameworkPropertyMetadata(OnIconChanged)); public static readonly DependencyProperty TextProperty = DependencyProperty.Register( "Text", typeof(string), typeof(NotifyIcon), new PropertyMetadata(OnTextChanged)); private Forms.NotifyIcon notifyIcon; static NotifyIcon() { VisibilityProperty.OverrideMetadata(typeof(NotifyIcon), new PropertyMetadata(OnVisibilityChanged)); } public event MouseButtonEventHandler MouseClick { add { this.AddHandler(MouseClickEvent, value); } remove { this.RemoveHandler(MouseClickEvent, value); } } public event MouseButtonEventHandler MouseDoubleClick { add { this.AddHandler(MouseDoubleClickEvent, value); } remove { this.RemoveHandler(MouseDoubleClickEvent, value); } } public ImageSource Icon { get { return (ImageSource)this.GetValue(IconProperty); } set { this.SetValue(IconProperty, value); } } public string Text { get { return (string)this.GetValue(TextProperty); } set { this.SetValue(TextProperty, value); } } public override void BeginInit() { base.BeginInit(); this.InitializeNotifyIcon(); } #region IAddChild Members void IAddChild.AddChild(object value) { throw new InvalidOperationException(); } void IAddChild.AddText(string text) { if (text == null) { throw new ArgumentNullException(nameof(text)); } this.Text = text; } #endregion protected override void OnVisualParentChanged(DependencyObject oldParent) { base.OnVisualParentChanged(oldParent); this.AttachToWindowClose(); } private static MouseButtonEventArgs CreateMouseButtonEventArgs( RoutedEvent handler, Forms.MouseButtons button) { return new MouseButtonEventArgs(InputManager.Current.PrimaryMouseDevice, 0, ToMouseButton(button)) { RoutedEvent = handler }; } private static Drawing.Icon? FromImageSource(ImageSource icon) { if (icon == null) { return null; } Uri iconUri = new Uri(icon.ToString()); return new Drawing.Icon(Application.GetResourceStream(iconUri).Stream); } private static void OnIconChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) { if (!DesignerProperties.GetIsInDesignMode(target)) { NotifyIcon control = (NotifyIcon)target; control.notifyIcon.Icon = FromImageSource(control.Icon); } } private static void OnTextChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) { NotifyIcon control = (NotifyIcon)target; control.notifyIcon.Text = control.Text; } private static void OnVisibilityChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) { NotifyIcon control = (NotifyIcon)target; control.notifyIcon.Visible = control.Visibility == Visibility.Visible; } private static MouseButton ToMouseButton(Forms.MouseButtons button) { switch (button) { case Forms.MouseButtons.Left: return MouseButton.Left; case Forms.MouseButtons.Right: return MouseButton.Right; case Forms.MouseButtons.Middle: return MouseButton.Middle; case Forms.MouseButtons.XButton1: return MouseButton.XButton1; case Forms.MouseButtons.XButton2: return MouseButton.XButton2; } throw new InvalidOperationException(); } private void AttachToWindowClose() { var window = Window.GetWindow(this); if (window != null) { window.Closed += (s, a) => this.notifyIcon.Dispose(); } } private void InitializeNotifyIcon() { this.notifyIcon = new Forms.NotifyIcon(); this.notifyIcon.Text = this.Text; this.notifyIcon.Icon = FromImageSource(this.Icon); this.notifyIcon.Visible = this.Visibility == Visibility.Visible; this.notifyIcon.MouseDown += this.OnMouseDown; this.notifyIcon.MouseUp += this.OnMouseUp; this.notifyIcon.MouseClick += this.OnMouseClick; this.notifyIcon.MouseDoubleClick += this.OnMouseDoubleClick; this.InitializeNativeHooks(); } private void OnMouseDown(object sender, Forms.MouseEventArgs e) { this.RaiseEvent(CreateMouseButtonEventArgs(MouseDownEvent, e.Button)); } private void OnMouseDoubleClick(object sender, Forms.MouseEventArgs e) { this.RaiseEvent(CreateMouseButtonEventArgs(MouseDoubleClickEvent, e.Button)); } private void OnMouseClick(object sender, Forms.MouseEventArgs e) { this.RaiseEvent(CreateMouseButtonEventArgs(MouseClickEvent, e.Button)); } private void OnMouseUp(object sender, Forms.MouseEventArgs e) { if (e.Button == Forms.MouseButtons.Right) { this.ShowContextMenu(); } this.RaiseEvent(CreateMouseButtonEventArgs(MouseUpEvent, e.Button)); } private void ShowContextMenu() { if (this.ContextMenu != null) { this.AttachContextMenu(); this.ContextMenu.IsOpen = true; } } partial void AttachContextMenu(); partial void InitializeNativeHooks(); } } ================================================ FILE: src/DesktopClient/PresenceLight/Services/Settings/AppPackageSettingsService.cs ================================================ using System; using Newtonsoft.Json; using PresenceLight.Core; using PresenceLight.Telemetry; using System.Threading.Tasks; using Windows.Storage; using Microsoft.Extensions.Logging; using System.IO; using PresenceLight.Razor; namespace PresenceLight.Services { public class AppPackageSettingsService : ISettingsService { private const string SETTINGS_FILENAME = "settings.json"; private static readonly StorageFolder _settingsFolder = Windows.Storage.ApplicationData.Current.LocalFolder; private DiagnosticsClient _diagClient; private readonly ILogger _logger; private readonly AppState _appState; public AppPackageSettingsService(DiagnosticsClient diagClient, ILogger logger, AppState appState) { _appState = appState; _logger = logger; _diagClient = diagClient; } public async Task LoadSettings() { try { StorageFile sf = await _settingsFolder.GetFileAsync(SETTINGS_FILENAME); if (sf == null) return null; string content = await FileIO.ReadTextAsync(sf, Windows.Storage.Streams.UnicodeEncoding.Utf8); var config = JsonConvert.DeserializeObject(content); _appState.SetConfig(config); return config; } catch (Exception e) { _logger.LogError(e, "Error saving Settings"); _diagClient.TrackException(e); return null; } } public async Task SaveSettings(BaseConfig data) { try { string content = JsonConvert.SerializeObject(data, Newtonsoft.Json.Formatting.Indented, new JsonSerializerSettings { }); StorageFile f; if (await IsFilePresent()) { f = await _settingsFolder.GetFileAsync(SETTINGS_FILENAME); } else { f = await _settingsFolder.CreateFileAsync(SETTINGS_FILENAME, CreationCollisionOption.ReplaceExisting); } bool fileWritten = false; while (!fileWritten) { try { await FileIO.WriteTextAsync(f, content, Windows.Storage.Streams.UnicodeEncoding.Utf8); fileWritten = true; } catch { } } _appState.SetConfig(data); return true; } catch (Exception e) { _logger.LogError(e, "Error Saving Settings"); _diagClient.TrackException(e); return false; } } public async Task DeleteSettings() { try { StorageFile sf = await _settingsFolder.GetFileAsync(SETTINGS_FILENAME); var foo = sf.DeleteAsync(StorageDeleteOption.PermanentDelete); return true; } catch (Exception e) { _logger.LogError(e, "Error Deleting Settings File"); _diagClient.TrackException(e); return false; } } public async Task IsFilePresent() { try { var item = await _settingsFolder.TryGetItemAsync(SETTINGS_FILENAME); if (item == null) { return false; } else { var config = await LoadSettings(); if (config == null) { return false; } } return true; } catch (Exception e) { _logger.LogError(e, "Error Finding Settings File"); _diagClient.TrackException(e); return false; } } public string GetSettingsFileLocation() { return BuildSettingsFileLocation(); } public static string BuildSettingsFileLocation() { return Path.Combine(_settingsFolder.Path, SETTINGS_FILENAME); } } } ================================================ FILE: src/DesktopClient/PresenceLight/Services/Settings/StandaloneSettingsService.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using PresenceLight.Core; using PresenceLight.Telemetry; using Newtonsoft.Json; using Microsoft.Extensions.Logging; using PresenceLight.Razor; namespace PresenceLight.Services { public class StandaloneSettingsService : ISettingsService { private const string _settingsFileName = "settings.json"; private static readonly string _settingsFolder = Directory.GetCurrentDirectory(); private DiagnosticsClient _diagClient; private readonly ILogger _logger; private readonly AppState _appState; public StandaloneSettingsService(DiagnosticsClient diagClient, ILogger logger, AppState appState) { _appState = appState; _logger = logger; _diagClient = diagClient; } public Task DeleteSettings() { if (File.Exists(GetSettingsFileLocation())) { File.Delete(GetSettingsFileLocation()); } return Task.Run(() => true); } public async Task IsFilePresent() { try { if (!File.Exists(GetSettingsFileLocation())) { return false; } else { var config = await LoadSettings(); if (config == null) { return false; } } return true; } catch (Exception e) { _logger.LogError(e, "Error Finding Settings File"); _diagClient.TrackException(e); return false; } } public async Task LoadSettings() { try { string fileJSON = await File.ReadAllTextAsync(GetSettingsFileLocation(), Encoding.UTF8); var config = JsonConvert.DeserializeObject(fileJSON); _appState.SetConfig(config); return config; } catch (Exception e) { _logger.LogError(e, "Error Loading Settings"); _diagClient.TrackException(e); return null; } } public async Task SaveSettings(BaseConfig data) { try { string content = JsonConvert.SerializeObject(data, Newtonsoft.Json.Formatting.Indented, new JsonSerializerSettings { }); await File.WriteAllTextAsync(GetSettingsFileLocation(), content, Encoding.UTF8); _appState.SetConfig(data); return true; } catch (Exception e) { _logger.LogError(e, "Error saving Settings"); _diagClient.TrackException(e); return false; } } public string GetSettingsFileLocation() { return BuildSettingsFileLocation(); } public static string BuildSettingsFileLocation() { return Path.Combine(_settingsFolder, _settingsFileName); } } } ================================================ FILE: src/DesktopClient/PresenceLight/Services/SingleInstanceAppMutex.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Services { class SingleInstanceAppMutex { private static Mutex s_mutex; public static bool TakeExclusivity() { var assembly = Assembly.GetExecutingAssembly(); var mutexName = $"Local\\{assembly.GetName().Name}-0e510f7b-aed2-40b0-ad72-d2d3fdc89a02"; s_mutex = new Mutex(true, mutexName, out bool mutexCreated); if (!mutexCreated) { Trace.WriteLine("SingleInstanceAppMutex TakeExclusivity: false"); s_mutex = null; return false; } return true; } public static void ReleaseExclusivity() { s_mutex?.ReleaseMutex(); s_mutex?.Close(); s_mutex = null; } } } ================================================ FILE: src/DesktopClient/PresenceLight/Services/Telemetry/DiagnosticsClient.cs ================================================ using System; using System.Collections.Generic; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; namespace PresenceLight.Telemetry { public class DiagnosticsClient { private TelemetryClient _client; public DiagnosticsClient(TelemetryClient tc) { _client = tc; TrackEvent("AppStart"); System.Windows.Application.Current.Exit += Application_Exit; System.Windows.Application.Current.DispatcherUnhandledException += DispatcherUnhandledException; } private void DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) { TrackException(e.Exception); e.Handled = true; } private void Application_Exit(object sender, System.Windows.ExitEventArgs e) { TrackEvent("AppExit"); _client.Flush(); // Allow time for flushing: System.Threading.Thread.Sleep(1000); } private void Application_Startup(object sender, System.Windows.StartupEventArgs e) { TrackEvent("AppStart"); } public void TrackEvent(string eventName, IDictionary? properties = null, IDictionary? metrics = null) { _client.TrackEvent(eventName, properties, metrics); } public void TrackTrace(string evt) { _client.TrackTrace(evt); } public void TrackException(Exception exception, IDictionary? properties = null, IDictionary? metrics = null) { _client.TrackException(exception, properties, metrics); } public void TrackPageView(string pageName) { _client.TrackPageView(pageName); } } } ================================================ FILE: src/DesktopClient/PresenceLight/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Information" } }, "StartMinimized": false, "IconType": "White", "AADSettings": { "ClientId": "", "TenantId": "common", "Instance": "https://login.microsoftonline.com/", "RedirectUri": "http://localhost", "Scopes": [ "https://graph.microsoft.com/.default" ] }, "LightSettings": { "HoursPassedStatus": "Keep", "SyncLights": true, "WorkingDays": "Monday|Tuesday|Wednesday|Thursday|Friday", "WorkingHoursStartTime": "", "WorkingHoursEndTime": "", "UseAmPm": true, "UseWorkingHours": false, "PollingInterval": 5.0, "UseDefaultBrightness": true, "DefaultBrightness": 100, "LIFX": { "LIFXClientId": "", "LIFXClientSecret": "", "LIFXApiKey": "", "IsEnabled": false, "SelectedItemId": "", "Brightness": 100, "UseActivityStatus": false, "Statuses": { "AvailabilityAvailableStatus": { "Disabled": false, "Color": "#00FF55" }, "AvailabilityAvailableIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityAwayStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBeRightBackStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBusyStatus": { "Disabled": false, "Color": "#FF3300" }, "AvailabilityBusyIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityDoNotDisturbStatus": { "Disabled": false, "Color": "#B03CDE" }, "AvailabilityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityOffStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityAvailableStatus": { "Disabled": false, "Color": "#4f824f" }, "ActivityAwayStatus": { "Disabled": false, "Color": "#FFFF00" }, "ActivityBeRightBackStatus": { "Disabled": false, "Color": "#fFFf00" }, "ActivityBusyStatus": { "Disabled": false, "Color": "#FF0000" }, "ActivityDoNotDisturbStatus": { "Disabled": false, "Color": "#960000" }, "ActivityInACallStatus": { "Disabled": false, "Color": "#FF0000" }, "ActivityInAConferenceCallStatus": { "Disabled": false, "Color": "#FF00d4" }, "ActivityInactiveStatus": { "Disabled": false, "Color": "#FF00" }, "ActivityInAMeetingStatus": { "Disabled": false, "Color": "#FF0000" }, "ActivityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOffWorkStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOutOfOfficeStatus": { "Disabled": false, "Color": "#ae00FF" }, "ActivityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityPresentingStatus": { "Disabled": false, "Color": "#960000" }, "ActivityUrgentInterruptionsOnlyStatus": { "Disabled": false, "Color": "#560061" }, "ActivityOffStatus": { "Disabled": false, "Color": "#FFFFFF" } } }, "Hue": { "HueApiKey": "", "SelectedItemId": "", "HueIpAddress": "", "RemoteHueClientId": "", "RemoteHueClientSecret": "", "RemoteHueClientAppName": "", "IsEnabled": false, "RemoteBridgeId": "", "UseRemoteApi": false, "Brightness": 100, "UseActivityStatus": false, "Statuses": { "AvailabilityAvailableStatus": { "Disabled": false, "Color": "#00FF55" }, "AvailabilityAvailableIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityAwayStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBeRightBackStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBusyStatus": { "Disabled": false, "Color": "#FF3300" }, "AvailabilityBusyIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityDoNotDisturbStatus": { "Disabled": false, "Color": "#B03CDE" }, "AvailabilityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityOffStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityAvailableStatus": { "Disabled": false, "Color": "#4f824f" }, "ActivityAwayStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBeRightBackStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBusyStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityDoNotDisturbStatus": { "Disabled": false, "Color": "#960000" }, "ActivityInACallStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityInAConferenceCallStatus": { "Disabled": false, "Color": "#ff00d4" }, "ActivityInactiveStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityInAMeetingStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOffWorkStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOutOfOfficeStatus": { "Disabled": false, "Color": "#ae00ff" }, "ActivityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityPresentingStatus": { "Disabled": false, "Color": "#960000" }, "ActivityUrgentInterruptionsOnlyStatus": { "Disabled": false, "Color": "#560061" }, "ActivityOffStatus": { "Disabled": false, "Color": "#FFFFFF" } } }, "Yeelight": { "SelectedItemId": "", "IsEnabled": false, "Brightness": 100, "UseActivityStatus": false, "Statuses": { "AvailabilityAvailableStatus": { "Disabled": false, "Color": "#00ff55" }, "AvailabilityAvailableIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityAwayStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBeRightBackStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBusyStatus": { "Disabled": false, "Color": "#FF3300" }, "AvailabilityBusyIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityDoNotDisturbStatus": { "Disabled": false, "Color": "#B03CDE" }, "AvailabilityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityOffStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityAvailableStatus": { "Disabled": false, "Color": "#4f824f" }, "ActivityAwayStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBeRightBackStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBusyStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityDoNotDisturbStatus": { "Disabled": false, "Color": "#960000" }, "ActivityInACallStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityInAConferenceCallStatus": { "Disabled": false, "Color": "#ff00d4" }, "ActivityInactiveStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityInAMeetingStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOffWorkStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOutOfOfficeStatus": { "Disabled": false, "Color": "#ae00ff" }, "ActivityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityPresentingStatus": { "Disabled": false, "Color": "#960000" }, "ActivityUrgentInterruptionsOnlyStatus": { "Disabled": false, "Color": "#560061" }, "ActivityOffStatus": { "Disabled": false, "Color": "#FFFFFF" } } }, "CustomApi": { "IsEnabled": false, "SelectedItemId": "", "Brightness": 100, "UseActivityStatus": false, "CustomApiAvailable": { "Method": "", "Uri": "", "Body": "" }, "CustomApiBusy": { "Method": "", "Uri": "", "Body": "" }, "CustomApiBeRightBack": { "Method": "", "Uri": "", "Body": "" }, "CustomApiAway": { "Method": "", "Uri": "", "Body": "" }, "CustomApiDoNotDisturb": { "Method": "", "Uri": "", "Body": "" }, "CustomApiOffline": { "Method": "", "Uri": "", "Body": "" }, "CustomApiOff": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityAvailable": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityInACall": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityInAConferenceCall": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityInAMeeting": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityPresenting": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityBusy": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityAway": { "Method": "", "Uri": "", "Body": "" }, "CustomApiAvailableIdle": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityBeRightBack": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityDoNotDisturb": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityIdle": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityOffline": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityOff": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityOffWork": { "Method": "", "Uri": "", "Body": "" }, "CustomApiTimeout": 100, "IgnoreCertificateErrors": false, "UseBasicAuth": false, "BasicAuthUserName": "", "BasicAuthUserPassword": "" }, "LocalSerialHost": { "IsEnabled": false, "SelectedItemId": "", "Brightness": 100, "UseActivityStatus": false, "BaudRate": "", "PortNumber": "", "LocalSerialHostMainSetup": { "BaudRate": "", "LineEnding": "", "Port": "" }, "LocalSerialHostAvailable": "", "LocalSerialHostBusy": "", "LocalSerialHostBeRightBack": "", "LocalSerialHostAway": "", "LocalSerialHostDoNotDisturb": "", "LocalSerialHostOffline": "", "LocalSerialHostOff": "", "LocalSerialHostActivityAvailable": "", "LocalSerialHostActivityInACall": "", "LocalSerialHostActivityInAConferenceCall": "", "LocalSerialHostActivityInAMeeting": "", "LocalSerialHostActivityPresenting": "", "LocalSerialHostActivityBusy": "", "LocalSerialHostActivityAway": "", "LocalSerialHostAvailableIdle": "", "LocalSerialHostActivityBeRightBack": "", "LocalSerialHostActivityDoNotDisturb": "", "LocalSerialHostActivityIdle": "", "LocalSerialHostActivityOffline": "", "LocalSerialHostActivityOff": "" }, "Wiz": { "SelectedItemId": "", "Brightness": 100, "IsEnabled": false, "UseActivityStatus": false, "Statuses": { "AvailabilityAvailableStatus": { "Disabled": false, "Color": "#00ff55" }, "AvailabilityAvailableIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityAwayStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBeRightBackStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBusyStatus": { "Disabled": false, "Color": "#FF3300" }, "AvailabilityBusyIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityDoNotDisturbStatus": { "Disabled": false, "Color": "#B03CDE" }, "AvailabilityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityOffStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityAvailableStatus": { "Disabled": false, "Color": "#4f824f" }, "ActivityAwayStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBeRightBackStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBusyStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityDoNotDisturbStatus": { "Disabled": false, "Color": "#960000" }, "ActivityInACallStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityInAConferenceCallStatus": { "Disabled": false, "Color": "#ff00d4" }, "ActivityInactiveStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityInAMeetingStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOffWorkStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOutOfOfficeStatus": { "Disabled": false, "Color": "#ae00ff" }, "ActivityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityPresentingStatus": { "Disabled": false, "Color": "#960000" }, "ActivityUrgentInterruptionsOnlyStatus": { "Disabled": false, "Color": "#560061" }, "ActivityOFFStatus": { "Disabled": false, "Color": "#FFFFFF" } } } }, "ApplicationInsights": { "TelemetryChannel": { "DeveloperMode": false }, "InstrumentationKey": "" }, "SnapshotCollectorConfiguration": { "IsEnabledInDeveloperMode": true, "ThresholdForSnapshotting": 1, "MaximumSnapshotsRequired": 3, "MaximumCollectionPlanSize": 50, "ReconnectInterval": "00:15:00", "ProblemCounterResetInterval": "1.00:00:00", "SnapshotsPerTenMinutesLimit": 1, "SnapshotsPerDayLimit": 30, "SnapshotInLowPriorityThread": true, "ProvideAnonymousTelemetry": true, "FailedRequestLimit": 3 }, "Serilog": { "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], "MinimumLevel": "Information", "WriteTo": [ { "Name": "Console" }, { "Name": "File", "Args": { "path": "%LOCALAPPDATA%/PresenceLight/logs/DesktopClient/log-.json", "formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog", "shared": "true", "rollingInterval": "Hour", "retainedFileCountLimit": 24 } } ], "Enrich": [ "FromLogContext", "WithThreadId" ], "Properties": { "Application": "PresenceLight" } }, "AppType": "Desktop", "AppVersion": "5.6.11" } ================================================ FILE: src/DesktopClient/PresenceLight/wwwroot/index.html ================================================  Blazor WPF app
An unhandled error has occurred. Reload 🗙
================================================ FILE: src/DesktopClient/PresenceLight.Package/Package-Local.appxmanifest ================================================  PresenceLight (Local) Isaac Levin Images\StoreLogo.png ================================================ FILE: src/DesktopClient/PresenceLight.Package/Package-Nightly.appxmanifest ================================================  PresenceLight (Nightly) Isaac Levin Images\StoreLogo.png ================================================ FILE: src/DesktopClient/PresenceLight.Package/Package.appinstaller ================================================ ================================================ FILE: src/DesktopClient/PresenceLight.Package/Package.appxmanifest ================================================  PresenceLight Isaac Levin Images\StoreLogo.png ================================================ FILE: src/DesktopClient/PresenceLight.Package/Package.xml ================================================  CN=82A4CBF8-0920-4FD3-B44C-A9D0D7BB5865 Isaac Levin AAD http://www.w3.org/2001/04/xmlenc#sha256 37828IsaacLevin.197278F15330A PresenceLight ================================================ FILE: src/DesktopClient/PresenceLight.Package/PresenceLight.Package.wapproj ================================================ 15.0 Debug x64 Release x64 Debug x86 x64 $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ 12ddad24-ccbd-409f-9342-17a0e445604f 10.0.19041.0 10.0.17134.0 en ManagedOnly false False Language|DXFeatureLevel Always x64|x86|ARM64 true SideloadOnly StoreUpload ..\PresenceLight\PresenceLight.csproj PresenceLight\PresenceLight.exe ..\PresenceLight\PresenceLight.csproj $(GetPackagingOutputsDependsOn);BuildRefsOutputGroup True https://presencelight.blob.core.windows.net/nightly 0 OnApplicationRun Designer Designer Designer True Properties\PublishProfiles\WinX64.pubxml Properties\PublishProfiles\WinARM64.pubxml Properties\PublishProfiles\WinX86.pubxml dotnet publish -c $(Configuration) "$(MSBuildThisFileDirectory)..\PresenceLight\PresenceLight.csproj" /p:PublishProfile=Properties\PublishProfiles\Win$(Platform).pubxml PreserveNewest <_PackagingOutputsUnexpanded Include="@(BuiltRef)" OutputGroup="BuildRefsOutputGroup" ProjectName="$(ProjectName)" /> ================================================ FILE: src/DockerCompose/.dockerignore ================================================ **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/azds.yaml **/bin **/charts **/docker-compose* **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ================================================ FILE: src/DockerCompose/docker-compose.dcproj ================================================ 2.1 Linux d6871e74-a6e2-41ee-aa4a-7be357501d63 LaunchBrowser {Scheme}://localhost:{ServicePort} presencelight.web Regular docker-compose.yml ================================================ FILE: src/DockerCompose/docker-compose.override.yml ================================================ version: '3.4' services: presencelight.web: environment: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=https://+:443;http://+:80 ports: - "5000:80" - "5001:443" volumes: - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro ================================================ FILE: src/DockerCompose/docker-compose.yml ================================================ version: '3.4' services: presencelight.web: image: ${DOCKER_REGISTRY-}presencelightweb build: context: .. dockerfile: PresenceLight.Web/Dockerfile ================================================ FILE: src/PresenceLight.Core/Configuration/AAD.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Newtonsoft.Json; namespace PresenceLight.Core { /// /// Represents the Azure Active Directory (AAD) settings. /// public class AADSettings { /// /// Gets or sets the client ID for AAD authentication. /// public string? ClientId { get; set; } /// /// Gets or sets the tenant ID for AAD authentication. /// public string? TenantId { get; set; } /// /// Gets or sets the Client Secret for AAD authentication. /// public string? ClientSecret { get; set; } /// /// Gets or sets the AAD instance URL. /// public string? Instance { get; set; } /// /// Gets or sets the redirect URI for AAD authentication. /// public string? RedirectUri { get; set; } /// /// Gets or sets the redirect host for AAD authentication. /// public string? RedirectHost { get; set; } /// /// Gets or sets the CallbackPath for AAD authentication. /// public string? CallbackPath { get; set; } /// /// Gets or sets the Scopes for AAD authentication. /// public List? Scopes { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/AppState.cs ================================================ using System; using System.Collections.Generic; using Microsoft.Graph; using Microsoft.Graph.Models; using PresenceLight.Core.WizServices; using Device = YeelightAPI.Device; namespace PresenceLight.Core { /// /// Represents the application state. /// public class AppState { /// /// Event that is triggered when the state changes. /// public event Action OnChange; /// /// Gets or sets the user information. /// public User User { get; set; } /// /// Gets or sets a value indicating whether the user is signed in. /// public bool SignedIn { get; set; } /// /// Gets or sets a value indicating whether the AAD config is complete. /// public bool AadConfigComplete { get; set; } /// /// Gets or sets a value indicating whether a sign-in request has been made. /// public bool SignInRequested { get; set; } /// /// Gets or sets a value indicating whether a sign-out request has been made. /// public bool SignOutRequested { get; set; } /// /// Gets or sets a value indicating whether a rebuild request has been made. /// public bool RebuildRequested { get; set; } /// /// Gets or sets the list of Hue lights. /// public IEnumerable HueLights { get; set; } /// /// Gets or sets the selected Hue light ID. /// public string HueLightId { get; set; } /// /// Gets or sets the list of Yeelight lights. /// public List YeelightLights { get; set; } /// /// Gets or sets the selected Yeelight light ID. /// public string YeelightLightId { get; set; } /// /// Gets or sets the list of local serial hosts. /// public IEnumerable LocalSerialHosts { get; set; } /// /// Gets or sets the selected local serial host. /// public string LocalSerialHostSelected { get; set; } /// /// Gets or sets the list of LIFX lights. /// public IEnumerable LIFXLights { get; set; } /// /// Gets or sets the selected LIFX light ID. /// public string LIFXLightId { get; set; } /// /// Gets or sets the Wiz light. /// public WizLight WizLight { get; set; } /// /// Gets or sets the profile image URL. /// public string ProfileImage { get; set; } /// /// Gets or sets the presence information. /// public Presence Presence { get; set; } /// /// Gets or sets the light mode. /// public string LightMode { get; set; } /// /// Gets or sets the custom color. /// public string CustomColor { get; set; } /// /// Gets or sets the base configuration. /// public BaseConfig Config { get; set; } = new BaseConfig(); /// /// Sets the configuration. /// /// The configuration to set. public void SetConfig(BaseConfig config) { Config = config; } /// /// Sets the user information. /// /// The user information. /// The presence information. /// The profile image URL. public void SetUserInfo(User? user, Presence? presence, string? photo = null) { User = user; Presence = presence; ProfileImage = photo; NotifyStateChanged(); } /// /// Sets the presence information. /// /// The presence information. public void SetPresence(Presence presence) { Presence = presence; NotifyStateChanged(); } /// /// Sets the custom color. /// /// The custom color. public void SetCustomColor(string color) { CustomColor = color; NotifyStateChanged(); } /// /// Sets the light mode. /// /// The light mode. public void SetLightMode(string lightMode) { LightMode = lightMode; NotifyStateChanged(); } /// /// Sets the list of Hue lights. /// /// The list of Hue lights. public void SetHueLights(IEnumerable lights) { HueLights = lights; NotifyStateChanged(); } /// /// Sets the selected Hue light ID. /// /// The selected Hue light ID. public void SetHueLight(string lightId) { HueLightId = lightId; NotifyStateChanged(); } /// /// Sets the list of Yeelight lights. /// /// The list of Yeelight lights. public void SetYeelightLights(List lights) { YeelightLights = lights; NotifyStateChanged(); } /// /// Sets the selected Yeelight light ID. /// /// The selected Yeelight light ID. public void SetYeelightLight(string lightId) { YeelightLightId = lightId; NotifyStateChanged(); } /// /// Sets the list of LIFX lights. /// /// The list of LIFX lights. public void SetLIFXLights(IEnumerable lights) { LIFXLights = lights; NotifyStateChanged(); } /// /// Sets the selected LIFX light ID. /// /// The selected LIFX light ID. public void SetLIFXLight(string lightId) { LIFXLightId = lightId; NotifyStateChanged(); } /// /// Sets the list of local serial hosts. /// /// The list of local serial hosts. public void SetLocalSerialHosts(IEnumerable lights) { LocalSerialHosts = lights; NotifyStateChanged(); } /// /// Sets the selected local serial host. /// /// The selected local serial host. public void SetLocalSerialHost(string port) { LocalSerialHostSelected = port; NotifyStateChanged(); } /// /// Sets the selected Wiz light. /// /// The selected Wiz light. public void SetWizLight(WizLight light) { WizLight = light; NotifyStateChanged(); } private void NotifyStateChanged() => OnChange?.Invoke(); } } ================================================ FILE: src/PresenceLight.Core/Configuration/AvailabilityStatus.cs ================================================ namespace PresenceLight.Core { /// /// Represents the availability status configuration. /// public class AvailabilityStatus { /// /// Gets or sets a value indicating whether the availability status is disabled. /// public bool Disabled { get; set; } /// /// Gets or sets the color associated with the availability status. /// public string? Color { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/Base.cs ================================================ namespace PresenceLight.Core { /// /// Represents the base configuration for the application. /// public class BaseConfig { /// /// Gets or sets a value indicating whether the application should start minimized. /// public bool StartMinimized { get; set; } /// /// Gets or sets the type of icon to be used. /// public string? IconType { get; set; } /// /// Gets or sets the light settings for the application. /// public LightSettings LightSettings { get; set; } /// /// Gets or sets the type of application. /// public string AppType { get; set; } /// /// Gets or sets the Microsoft Entra Settings. /// public AADSettings AADSettings { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/BaseLight.cs ================================================ namespace PresenceLight.Core { /// /// Represents a base light configuration. /// public class BaseLight { /// /// Gets or sets a value indicating whether the light is enabled. /// public bool IsEnabled { get; set; } /// /// Gets or sets the selected item ID. /// public string? SelectedItemId { get; set; } /// /// Gets or sets the brightness level of the light. /// public int Brightness { get; set; } /// /// Gets or sets the presence light statuses. /// public PresenceLightStatuses Statuses { get; set; } /// /// Gets or sets a value indicating whether to use activity status for the light. /// public bool UseActivityStatus { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/CustomApi.cs ================================================ namespace PresenceLight.Core { /// /// Represents the configuration settings for a custom API. /// public class CustomApi : BaseLight { /// /// Gets or sets the timeout value for the custom API. /// public double CustomApiTimeout { get; set; } /// /// Gets or sets a value indicating whether to ignore certificate errors for the custom API. /// public bool IgnoreCertificateErrors { get; set; } /// /// Gets or sets a value indicating whether to use basic authentication for the custom API. /// public bool UseBasicAuth { get; set; } /// /// Gets or sets the username for basic authentication for the custom API. /// public string BasicAuthUserName { get; set; } /// /// Gets or sets the password for basic authentication for the custom API. /// public string BasicAuthUserPassword { get; set; } /// /// Gets or sets the configuration settings for the "Available" status of the custom API. /// public CustomApiSetting CustomApiAvailable { get; set; } /// /// Gets or sets the configuration settings for the "Busy" status of the custom API. /// public CustomApiSetting CustomApiBusy { get; set; } /// /// Gets or sets the configuration settings for the "Be Right Back" status of the custom API. /// public CustomApiSetting CustomApiBeRightBack { get; set; } /// /// Gets or sets the configuration settings for the "Away" status of the custom API. /// public CustomApiSetting CustomApiAway { get; set; } /// /// Gets or sets the configuration settings for the "Do Not Disturb" status of the custom API. /// public CustomApiSetting CustomApiDoNotDisturb { get; set; } /// /// Gets or sets the configuration settings for the "Available Idle" status of the custom API. /// public CustomApiSetting CustomApiAvailableIdle { get; set; } /// /// Gets or sets the configuration settings for the "Offline" status of the custom API. /// public CustomApiSetting CustomApiOffline { get; set; } /// /// Gets or sets the configuration settings for the "Off" status of the custom API. /// public CustomApiSetting CustomApiOff { get; set; } /// /// Gets or sets the configuration settings for the "Activity: Available" status of the custom API. /// public CustomApiSetting CustomApiActivityAvailable { get; set; } /// /// Gets or sets the configuration settings for the "Activity: In a Call" status of the custom API. /// public CustomApiSetting CustomApiActivityInACall { get; set; } /// /// Gets or sets the configuration settings for the "Activity: In a Conference Call" status of the custom API. /// public CustomApiSetting CustomApiActivityInAConferenceCall { get; set; } /// /// Gets or sets the configuration settings for the "Activity: In a Meeting" status of the custom API. /// public CustomApiSetting CustomApiActivityInAMeeting { get; set; } /// /// Gets or sets the configuration settings for the "Activity: Presenting" status of the custom API. /// public CustomApiSetting CustomApiActivityPresenting { get; set; } /// /// Gets or sets the configuration settings for the "Activity: Busy" status of the custom API. /// public CustomApiSetting CustomApiActivityBusy { get; set; } /// /// Gets or sets the configuration settings for the "Activity: Away" status of the custom API. /// public CustomApiSetting CustomApiActivityAway { get; set; } /// /// Gets or sets the configuration settings for the "Activity: Be Right Back" status of the custom API. /// public CustomApiSetting CustomApiActivityBeRightBack { get; set; } /// /// Gets or sets the configuration settings for the "Activity: Do Not Disturb" status of the custom API. /// public CustomApiSetting CustomApiActivityDoNotDisturb { get; set; } /// /// Gets or sets the configuration settings for the "Activity: Idle" status of the custom API. /// public CustomApiSetting CustomApiActivityIdle { get; set; } /// /// Gets or sets the configuration settings for the "Activity: Offline" status of the custom API. /// public CustomApiSetting CustomApiActivityOffline { get; set; } /// /// Gets or sets the configuration settings for the "Activity: Off" status of the custom API. /// public CustomApiSetting CustomApiActivityOff { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/CustomApiSetting.cs ================================================ namespace PresenceLight.Core { /// /// Represents the settings for a custom API. /// public class CustomApiSetting { /// /// Gets or sets the HTTP method used for the API. /// public string? Method { get; set; } /// /// Gets or sets the URI of the API. /// public string? Uri { get; set; } /// /// Gets or sets the Body of the API. /// public string? Body { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/Hue.cs ================================================ using System.ComponentModel.DataAnnotations; using Newtonsoft.Json; namespace PresenceLight.Core { /// /// Represents the configuration for Hue lights. /// public class Hue : BaseLight { /// /// Gets or sets the client ID for remote Hue access. /// public string? RemoteHueClientId { get; set; } /// /// Gets or sets the client application name for remote Hue access. /// public string? RemoteHueClientAppName { get; set; } /// /// Gets or sets the client secret for remote Hue access. /// public string? RemoteHueClientSecret { get; set; } /// /// Gets or sets the API key for local Hue access. /// public string? HueApiKey { get; set; } /// /// Gets or sets the IP address of the Hue bridge. /// [Required] [RegularExpression(@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", ErrorMessage = "Not a valid IP Address")] public string? HueIpAddress { get; set; } /// /// Gets or sets a value indicating whether to use remote API for Hue access. /// public bool UseRemoteApi { get; set; } /// /// Gets or sets the bridge ID for remote Hue access. /// public string RemoteBridgeId { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/ISettingsService.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using PresenceLight.Core; namespace PresenceLight.Core { /// /// Represents a service for managing application settings. /// public interface ISettingsService { /// /// Loads the application settings from a file. /// /// The loaded settings, or null if the file is not found. public Task LoadSettings(); /// /// Saves the application settings to a file. /// /// The settings to save. /// True if the settings are successfully saved, false otherwise. public Task SaveSettings(BaseConfig data); /// /// Deletes the application settings file. /// /// True if the settings file is successfully deleted, false otherwise. public Task DeleteSettings(); /// /// Checks if the application settings file is present. /// /// True if the settings file is present, false otherwise. public Task IsFilePresent(); /// /// Gets the location of the application settings file. /// /// The file location. public string GetSettingsFileLocation(); } } ================================================ FILE: src/PresenceLight.Core/Configuration/LIFX.cs ================================================ namespace PresenceLight.Core { /// /// Represents the configuration for LIFX lights. /// public class LIFX : BaseLight { /// /// Gets or sets the LIFX API key. /// public string? LIFXApiKey { get; set; } /// /// Gets or sets the LIFX client ID. /// public string? LIFXClientId { get; set; } /// /// Gets or sets the LIFX client secret. /// public string? LIFXClientSecret { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/LightSettings.cs ================================================ using System; using Newtonsoft.Json; namespace PresenceLight.Core { /// /// Represents the settings for controlling the lights. /// public class LightSettings { /// /// Gets or sets the status to display after a certain number of hours have passed. /// public string HoursPassedStatus { get; set; } /// /// Gets or sets a value indicating whether to synchronize the lights. /// public bool SyncLights { get; set; } /// /// Gets or sets the working days. /// public string WorkingDays { get; set; } /// /// Gets or sets a value indicating whether to use working hours. /// public bool UseWorkingHours { get; set; } /// /// Gets or sets a value indicating whether to use AM/PM format for working hours. /// public bool UseAmPm { get; set; } /// /// Gets or sets the start time of working hours. /// public string WorkingHoursStartTime { get; set; } /// /// Gets or sets the start time of working hours as a object. /// [Newtonsoft.Json.JsonIgnore] [System.Text.Json.Serialization.JsonIgnore] [JsonProperty(Required = Required.Default)] public DateTime? WorkingHoursStartTimeAsDate { get; set; } /// /// Gets or sets the end time of working hours as a object. /// [Newtonsoft.Json.JsonIgnore] [System.Text.Json.Serialization.JsonIgnore] [JsonProperty(Required = Required.Default)] public DateTime? WorkingHoursEndTimeAsDate { get; set; } /// /// Gets or sets the end time of working hours. /// public string WorkingHoursEndTime { get; set; } /// /// Gets or sets the polling interval in seconds. /// public double PollingInterval { get; set; } /// /// Gets or sets a value indicating whether to use the default brightness. /// public bool UseDefaultBrightness { get; set; } /// /// Gets or sets the default brightness level. /// public int DefaultBrightness { get; set; } /// /// Gets or sets the custom API settings. /// public CustomApi CustomApi { get; set; } /// /// Gets or sets the local serial host settings. /// public LocalSerialHost LocalSerialHost { get; set; } /// /// Gets or sets the LIFX settings. /// public LIFX LIFX { get; set; } /// /// Gets or sets the Hue settings. /// public Hue Hue { get; set; } /// /// Gets or sets the Yeelight settings. /// public Yeelight Yeelight { get; set; } /// /// Gets or sets the Wiz settings. /// public Wiz Wiz { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/LocalSerialHost.cs ================================================ namespace PresenceLight.Core { /// /// Represents the configuration settings for a local serial host. /// public class LocalSerialHost : BaseLight { /// /// Gets or sets the main setup for the local serial host. /// public LocalSerialHostSetting LocalSerialHostMainSetup { get; set; } /// /// Gets or sets the available status for the local serial host. /// public string LocalSerialHostAvailable { get; set; } /// /// Gets or sets the busy status for the local serial host. /// public string LocalSerialHostBusy { get; set; } /// /// Gets or sets the be right back status for the local serial host. /// public string LocalSerialHostBeRightBack { get; set; } /// /// Gets or sets the away status for the local serial host. /// public string LocalSerialHostAway { get; set; } /// /// Gets or sets the do not disturb status for the local serial host. /// public string LocalSerialHostDoNotDisturb { get; set; } /// /// Gets or sets the available idle status for the local serial host. /// public string LocalSerialHostAvailableIdle { get; set; } /// /// Gets or sets the offline status for the local serial host. /// public string LocalSerialHostOffline { get; set; } /// /// Gets or sets the off status for the local serial host. /// public string LocalSerialHostOff { get; set; } /// /// Gets or sets the available activity status for the local serial host. /// public string LocalSerialHostActivityAvailable { get; set; } /// /// Gets or sets the in a call activity status for the local serial host. /// public string LocalSerialHostActivityInACall { get; set; } /// /// Gets or sets the in a conference call activity status for the local serial host. /// public string LocalSerialHostActivityInAConferenceCall { get; set; } /// /// Gets or sets the in a meeting activity status for the local serial host. /// public string LocalSerialHostActivityInAMeeting { get; set; } /// /// Gets or sets the presenting activity status for the local serial host. /// public string LocalSerialHostActivityPresenting { get; set; } /// /// Gets or sets the busy activity status for the local serial host. /// public string LocalSerialHostActivityBusy { get; set; } /// /// Gets or sets the away activity status for the local serial host. /// public string LocalSerialHostActivityAway { get; set; } /// /// Gets or sets the be right back activity status for the local serial host. /// public string LocalSerialHostActivityBeRightBack { get; set; } /// /// Gets or sets the do not disturb activity status for the local serial host. /// public string LocalSerialHostActivityDoNotDisturb { get; set; } /// /// Gets or sets the idle activity status for the local serial host. /// public string LocalSerialHostActivityIdle { get; set; } /// /// Gets or sets the offline activity status for the local serial host. /// public string LocalSerialHostActivityOffline { get; set; } /// /// Gets or sets the off activity status for the local serial host. /// public string LocalSerialHostActivityOff { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/LocalSerialHostSetting.cs ================================================ namespace PresenceLight.Core { /// /// Represents the settings for a local serial host. /// public class LocalSerialHostSetting { /// /// Gets or sets the baud rate for the serial communication. /// public string? BaudRate { get; set; } /// /// Gets or sets the line ending characters for the serial communication. /// public string? LineEnding { get; set; } /// /// Gets or sets the port name for the serial communication. /// public string? Port { get; set; } /// /// Gets or sets the message to be sent over the serial communication. /// public string? Message { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/Statuses.cs ================================================ using System; using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Newtonsoft.Json; namespace PresenceLight.Core { /// /// Represents the statuses for availability and activity. /// public class PresenceLightStatuses { /// /// Gets or sets the availability status for "Available". /// public AvailabilityStatus AvailabilityAvailableStatus { get; set; } /// /// Gets or sets the availability status for "Available (Idle)". /// public AvailabilityStatus AvailabilityAvailableIdleStatus { get; set; } /// /// Gets or sets the availability status for "Away". /// public AvailabilityStatus AvailabilityAwayStatus { get; set; } /// /// Gets or sets the availability status for "Be Right Back". /// public AvailabilityStatus AvailabilityBeRightBackStatus { get; set; } /// /// Gets or sets the availability status for "Busy". /// public AvailabilityStatus AvailabilityBusyStatus { get; set; } /// /// Gets or sets the availability status for "Busy (Idle)". /// public AvailabilityStatus AvailabilityBusyIdleStatus { get; set; } /// /// Gets or sets the availability status for "Do Not Disturb". /// public AvailabilityStatus AvailabilityDoNotDisturbStatus { get; set; } /// /// Gets or sets the availability status for "Offline". /// public AvailabilityStatus AvailabilityOfflineStatus { get; set; } /// /// Gets or sets the availability status for "Presence Unknown". /// public AvailabilityStatus AvailabilityPresenceUnknownStatus { get; set; } /// /// Gets or sets the availability status for "Off". /// public AvailabilityStatus AvailabilityOffStatus { get; set; } /// /// Gets or sets the activity status for "Available". /// public AvailabilityStatus ActivityAvailableStatus { get; set; } /// /// Gets or sets the activity status for "Away". /// public AvailabilityStatus ActivityAwayStatus { get; set; } /// /// Gets or sets the activity status for "Be Right Back". /// public AvailabilityStatus ActivityBeRightBackStatus { get; set; } /// /// Gets or sets the activity status for "Busy". /// public AvailabilityStatus ActivityBusyStatus { get; set; } /// /// Gets or sets the activity status for "Do Not Disturb". /// public AvailabilityStatus ActivityDoNotDisturbStatus { get; set; } /// /// Gets or sets the activity status for "In a Call". /// public AvailabilityStatus ActivityInACallStatus { get; set; } /// /// Gets or sets the activity status for "In a Conference Call". /// public AvailabilityStatus ActivityInAConferenceCallStatus { get; set; } /// /// Gets or sets the activity status for "Inactive". /// public AvailabilityStatus ActivityInactiveStatus { get; set; } /// /// Gets or sets the activity status for "In a Meeting". /// public AvailabilityStatus ActivityInAMeetingStatus { get; set; } /// /// Gets or sets the activity status for "Offline". /// public AvailabilityStatus ActivityOfflineStatus { get; set; } /// /// Gets or sets the activity status for "Off". /// public AvailabilityStatus ActivityOffStatus { get; set; } /// /// Gets or sets the activity status for "Off Work". /// public AvailabilityStatus ActivityOffWorkStatus { get; set; } /// /// Gets or sets the activity status for "Out of Office". /// public AvailabilityStatus ActivityOutOfOfficeStatus { get; set; } /// /// Gets or sets the activity status for "Presence Unknown". /// public AvailabilityStatus ActivityPresenceUnknownStatus { get; set; } /// /// Gets or sets the activity status for "Presenting". /// public AvailabilityStatus ActivityPresentingStatus { get; set; } /// /// Gets or sets the activity status for "Urgent Interruptions Only". /// public AvailabilityStatus ActivityUrgentInterruptionsOnlyStatus { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/Wiz.cs ================================================ using System.ComponentModel.DataAnnotations; namespace PresenceLight.Core { public class Wiz : BaseLight { [Required] [RegularExpression(@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", ErrorMessage = "Not a valid IP Address")] public string? IPAddress { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Configuration/Yeelight.cs ================================================ using System; using System.Text.Json.Serialization; namespace PresenceLight.Core { public class Yeelight : BaseLight { } } ================================================ FILE: src/PresenceLight.Core/GraphServices/AuthorizationProvider.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Graph; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Authentication; namespace PresenceLight.Core { public class AuthorizationProvider : IAuthenticationProvider { public IPublicClientApplication PubClient { get; set; } public IConfidentialClientApplication ConfClient { get; set; } private readonly ILogger _logger; private readonly IOptionsMonitor _configMonitor; private readonly IDisposable? _reloadSubscription; private readonly object _sync = new(); public IAccount UserAccount { get; set; } public AuthorizationProvider(IOptionsMonitor configMonitor, ILogger logger) { _logger = logger; _configMonitor = configMonitor; } public bool RebuildMsalClients() { lock (_sync) { BaseConfig config = _configMonitor.CurrentValue; if (config.AppType == "Desktop") { if (!Helpers.AreStringsNotEmpty(new string[] { config.AADSettings.ClientId, config.AADSettings.TenantId, config.AADSettings.Instance, config.AADSettings.RedirectUri })) { _logger.LogWarning("One or more of ClientId, TenantId, Instance, or RedirectUri is not set."); PubClient = null; return false; } PubClient = PublicClientApplicationBuilder.Create(config.AADSettings.ClientId) .WithAuthority($"{config.AADSettings.Instance}{config.AADSettings.TenantId}/") .WithRedirectUri(config.AADSettings.RedirectUri) .Build(); TokenCacheHelper.EnableSerialization(PubClient.UserTokenCache); return true; } else if (config.AppType == "Web") { if (!Helpers.AreStringsNotEmpty(new string[] { config.AADSettings.ClientId, config.AADSettings.ClientSecret, config.AADSettings.Instance, config.AADSettings.RedirectHost, config.AADSettings.CallbackPath })) { _logger.LogWarning("One or more of ClientId, ClientSecret, Instance, RedirectUri, or CallbackPath is not set."); ConfClient = null; return false; } ConfClient = ConfidentialClientApplicationBuilder .Create(config.AADSettings.ClientId) .WithClientSecret(config.AADSettings.ClientSecret) .WithAuthority($"{config.AADSettings.Instance}{config.AADSettings.TenantId}/v2.0") .WithRedirectUri($"{config.AADSettings.RedirectHost}{config.AADSettings.CallbackPath}") .Build(); var cacheHelper = CreateCacheHelperAsync(config.AADSettings.ClientId); cacheHelper.RegisterCache(ConfClient.UserTokenCache); return true; } } return false; } public void Invalidate() { lock (_sync) { PubClient = null; ConfClient = null; UserAccount = null; } } public async Task AcquireToken() { AuthenticationResult authResult = null; string accessToken = null; BaseConfig config = _configMonitor.CurrentValue; if (config.AppType == "Desktop") { var accounts = await PubClient.GetAccountsAsync(); var firstAccount = accounts.FirstOrDefault(); try { _logger.LogTrace("Acquiring token silently"); authResult = await PubClient.AcquireTokenSilent(config.AADSettings.Scopes, accounts.FirstOrDefault()) .ExecuteAsync(); UserAccount = authResult.Account; accessToken = authResult.AccessToken; _logger.LogDebug("Got tokens silently"); } catch (MsalUiRequiredException) { _logger.LogInformation("Silent token acquisition failed. Falling back to interactive."); try { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); //without a timeout, this hangs indefinitely if the browser/tab is closed authResult = await PubClient.AcquireTokenInteractive(config.AADSettings.Scopes) .WithUseEmbeddedWebView(false) .ExecuteAsync(cts.Token); _logger.LogTrace("Getting tokens interactively"); UserAccount = authResult.Account; _logger.LogDebug("Got user account"); accessToken = authResult.AccessToken; _logger.LogDebug("Got tokens interactively"); } catch (MsalException ex) when (ex.ErrorCode == "access_denied") { // User closed the browser / cancelled login _logger.LogWarning("User canceled interactive login"); } catch (MsalException ex) when (ex.ErrorCode == "consent_required") { _logger.LogWarning("User did not consent to the application"); } catch (MsalException ex) when (ex.ErrorCode == "invalid_request") { if (ex.Message != null && ex.Message.Contains("not configured as a multi-tenant application")) { _logger.LogWarning("Application is not configured as multi-tenant"); } else { _logger.LogWarning(ex, "Interactive token acquisition failed"); } } catch (OperationCanceledException) { _logger.LogWarning("Interactive login canceled or timed out"); } catch (Exception ex) { _logger.LogWarning(ex, "Interactive token acquisition failed"); } } } else if (config.AppType == "Web") { try { var result = await ConfClient .AcquireTokenSilent(config.AADSettings.Scopes, UserAccount) .ExecuteAsync(); UserAccount = result.Account; accessToken = result.AccessToken; } catch (System.Exception) { } } return accessToken; } private static MsalCacheHelper CreateCacheHelperAsync(string clientId) { StorageCreationProperties storageProperties; try { storageProperties = new StorageCreationPropertiesBuilder( "cache.plaintext", System.AppContext.BaseDirectory, clientId) .WithLinuxUnprotectedFile() .Build(); var cacheHelper = MsalCacheHelper.CreateAsync(storageProperties).Result; return cacheHelper; } catch (MsalCachePersistenceException e) { storageProperties = new StorageCreationPropertiesBuilder( "cache.plaintext", System.AppContext.BaseDirectory, clientId) .WithLinuxUnprotectedFile() .Build(); var cacheHelper = MsalCacheHelper.CreateAsync(storageProperties).Result; cacheHelper.VerifyPersistence(); return cacheHelper; } } async Task IAuthenticationProvider.AuthenticateRequestAsync(RequestInformation request, Dictionary? additionalAuthenticationContext, CancellationToken cancellationToken) { string accessToken = await AcquireToken(); if (!string.IsNullOrEmpty(accessToken)) { request.Headers.Add("Authorization", $"Bearer {accessToken}"); } } public static bool AadChanged(BaseConfig config, BaseConfig newConfig) { if (config.AppType != newConfig.AppType) { return true; } else if (config.AppType == "Desktop") { bool aadChanged = config.AADSettings.ClientId != newConfig.AADSettings.ClientId || config.AADSettings.TenantId != newConfig.AADSettings.TenantId || config.AADSettings.Instance != newConfig.AADSettings.Instance || config.AADSettings.RedirectUri != newConfig.AADSettings.RedirectUri; return aadChanged; } else if (config.AppType == "Web") { bool aadChanged = config.AADSettings.ClientId != newConfig.AADSettings.ClientId || config.AADSettings.TenantId != newConfig.AADSettings.TenantId || config.AADSettings.Instance != newConfig.AADSettings.Instance || config.AADSettings.CallbackPath != newConfig.AADSettings.CallbackPath || config.AADSettings.RedirectHost != newConfig.AADSettings.RedirectHost || config.AADSettings.ClientSecret != newConfig.AADSettings.ClientSecret; return aadChanged; } return false; } } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GetIsInitialized/GetIsInitializedCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.GraphServices { public class GetIsInitializedCommand : IRequest { } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GetIsInitialized/GetIsInitializedHandler.cs ================================================ using MediatR; using System; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.GraphServices { internal class GetIsInitializedHandler : IRequestHandler { GraphWrapper _graph; public GetIsInitializedHandler(GraphWrapper graph) { _graph = graph; } public async Task Handle(GetIsInitializedCommand command, CancellationToken cancellationToken) { return await Task.FromResult(_graph.IsInitialized); } } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GetPhoto/GetPhotoCommand.cs ================================================ using MediatR; using System.IO; namespace PresenceLight.Core.GraphServices { public class GetPhotoCommand : IRequest { } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GetPhoto/GetPhotoHandler.cs ================================================ using MediatR; using Microsoft.Graph; using Polly.Retry; using System.IO; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.GraphServices { internal class GetPhotoHandler : IRequestHandler { GraphWrapper _graph; public GetPhotoHandler(GraphWrapper graph) { _graph = graph; } public async Task Handle(GetPhotoCommand command, CancellationToken cancellationToken) { return await _graph.GetPhoto(cancellationToken); } } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GetPresence/GetPresenceCommand.cs ================================================ using MediatR; using Microsoft.Graph.Models; namespace PresenceLight.Core.GraphServices { public class GetPresenceCommand : IRequest { } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GetPresence/GetPresenceHandler.cs ================================================ using MediatR; using Microsoft.Graph.Models; using Polly.Retry; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.GraphServices { internal class GetPresenceHandler : IRequestHandler { GraphWrapper _graph; public GetPresenceHandler(GraphWrapper graph) { _graph = graph; } public async Task Handle(GetPresenceCommand command, CancellationToken cancellationToken) { return await _graph.GetPresence(cancellationToken); } } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GetProfile/GetProfileCommand.cs ================================================ using MediatR; using Microsoft.Graph.Models; namespace PresenceLight.Core.GraphServices { public class GetProfileCommand : IRequest { } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GetProfile/GetProfileHandler.cs ================================================ using MediatR; using Microsoft.Graph.Models; using Polly.Retry; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.GraphServices { internal class GetProfileHandler : IRequestHandler { GraphWrapper _graph; public GetProfileHandler(GraphWrapper graph) { _graph = graph; } public async Task Handle(GetProfileCommand command, CancellationToken cancellationToken) { return await _graph.GetProfile(cancellationToken); } } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GetProfileAndPresence/GetProfileAndPresenceCommand.cs ================================================ using MediatR; using Microsoft.Graph.Models; using System; namespace PresenceLight.Core.GraphServices { public class GetProfileAndPresenceCommand : IRequest<(User User, Presence Presence)> { } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GetProfileAndPresence/GetProfileAndPresenceHandler.cs ================================================ using MediatR; using Microsoft.Graph.Models; using Polly; using Polly.Retry; using System; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.GraphServices { internal class GetProfileAndPresenceHandler : IRequestHandler { GraphWrapper _graph; public GetProfileAndPresenceHandler(GraphWrapper graph) { _graph = graph; } public async Task<(User User, Presence Presence)> Handle(GetProfileAndPresenceCommand command, CancellationToken cancellationToken) { return await _graph.GetProfileAndPresence(cancellationToken); } } } ================================================ FILE: src/PresenceLight.Core/GraphServices/GraphWrapper.cs ================================================ using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Graph; using Microsoft.Graph.Models; using Microsoft.Graph.Models.ODataErrors; using Polly; using Polly.Retry; namespace PresenceLight.Core { public class GraphWrapper { private readonly ILogger _logger; private GraphServiceClient _graphServiceClient; internal AsyncRetryPolicy _retryPolicy; private LoginService loginService; public bool IsInitialized { get { if (loginService != null) { return loginService.IsInitialized; } return false; } } public GraphWrapper(ILogger logger, LoginService _loginService) { _logger = logger; loginService = _loginService; _retryPolicy = Policy .Handle() .WaitAndRetryAsync(2, retryAttempt => { var timeToWait = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)); return timeToWait; } ); } public async Task Initialize() { await loginService.GetAuthenticatedGraphClient(); _graphServiceClient = loginService.GraphServiceClient; } public async Task GetPresence(CancellationToken token) { try { return await _retryPolicy.ExecuteAsync(async () => await _graphServiceClient.Me.Presence.GetAsync().ConfigureAwait(true)); } catch (Exception ex) { _logger.LogError(ex, "Error Occurred Getting Presence from Graph Api Content"); throw; } } public async Task GetPhoto(CancellationToken token) { try { return await _retryPolicy.ExecuteAsync(async () => await _graphServiceClient.Me.Photo.Content.GetAsync().ConfigureAwait(true)); } catch (ODataError ex) { if ("ImageNotFound".Equals(ex.Error.Code)) { _logger.LogInformation(ex, "Profile photo does not exist"); return null; } else { throw; } } } public async Task GetProfile(CancellationToken token) { try { return await _retryPolicy.ExecuteAsync(async () => await _graphServiceClient.Me.GetAsync().ConfigureAwait(true)); } catch (Exception e) { _logger.LogError(e, "Error Occurred Getting Profile from Graph Api"); throw; } } public async Task<(User User, Presence Presence)> GetProfileAndPresence(CancellationToken token) { return await _retryPolicy.ExecuteAsync<(User User, Presence Presence)>(async () => await GetBatchContent(token)); } private async Task<(User User, Presence Presence)> GetBatchContent(CancellationToken token) { _logger.LogInformation("Getting Graph Data: Profile, Image, Presence"); try { var userRequest = _graphServiceClient.Me.ToGetRequestInformation(); var presenceRequest = _graphServiceClient.Me.Presence.ToGetRequestInformation(); BatchRequestContentCollection batchRequestContent = new BatchRequestContentCollection(_graphServiceClient); var userRequestId = await batchRequestContent.AddBatchRequestStepAsync(userRequest); var presenceRequestId = await batchRequestContent.AddBatchRequestStepAsync(presenceRequest); var returnedResponse = await _graphServiceClient.Batch.PostAsync(batchRequestContent); var user = await returnedResponse.GetResponseByIdAsync(userRequestId); var presence = await returnedResponse.GetResponseByIdAsync(presenceRequestId); return (User: user, Presence: presence); } catch (Exception e) { _logger.LogError(e, "Error Occurred Getting Batch Content from Graph Api"); throw; } } } } ================================================ FILE: src/PresenceLight.Core/GraphServices/Initialize/InitializeCommand.cs ================================================ using MediatR; using Microsoft.Graph; using System; namespace PresenceLight.Core.GraphServices { public class InitializeCommand : IRequest { //public GraphServiceClient Client { get; set; } } } ================================================ FILE: src/PresenceLight.Core/GraphServices/Initialize/InitializeHandler.cs ================================================ using System.Threading; using System.Threading.Tasks; using MediatR; namespace PresenceLight.Core.GraphServices { internal class InitializeHandler : IRequestHandler { GraphWrapper _graph; public InitializeHandler(GraphWrapper graph) { _graph = graph; } async Task IRequestHandler.Handle(InitializeCommand command, CancellationToken cancellationToken) { await _graph.Initialize(); await Task.CompletedTask; } } } ================================================ FILE: src/PresenceLight.Core/GraphServices/LoginService.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Graph; namespace PresenceLight.Core { public class LoginService { public GraphServiceClient GraphServiceClient { get; set; } private readonly AuthorizationProvider _authProvider; private readonly ILogger _logger; private readonly IOptionsMonitor _configMonitor; private readonly IDisposable? _reloadSubscription; private readonly AppState _appState; private readonly object _sync = new(); public bool IsInitialized { get; set; } private BaseConfig config; public LoginService(IOptionsMonitor configMonitor, AuthorizationProvider authProvider, AppState appState, ILogger logger) { _authProvider = authProvider; _appState = appState; _logger = logger; _configMonitor = configMonitor; config = _configMonitor.CurrentValue; _appState.AadConfigComplete = _authProvider.RebuildMsalClients(); _reloadSubscription = _configMonitor.OnChange(async newConfig => { try { if (AuthorizationProvider.AadChanged(config, newConfig)) { _logger?.LogInformation("AAD settings changed; signing out and invalidating auth provider."); if (_appState.SignInRequested) { _appState.SignInRequested = false; } _appState.RebuildRequested = true; } } catch (Exception ex) { _logger?.LogWarning(ex, "Error reacting to AAD settings change."); } config = newConfig; }); } public async Task GetAuthenticatedGraphClient() { await _authProvider.AcquireToken(); if (_authProvider.UserAccount != null) { GraphServiceClient = new GraphServiceClient(_authProvider); IsInitialized = true; } } public async Task IsUserAuthenticated() { BaseConfig config = _configMonitor.CurrentValue; // If we already have the user account we're // authenticated if (_authProvider.UserAccount != null) { return true; } if (config.AppType == "Desktop") { if (_authProvider.PubClient == null) { return false; } var accounts = await _authProvider.PubClient.GetAccountsAsync(); _authProvider.UserAccount = accounts.FirstOrDefault(); return null != _authProvider.UserAccount; } else if (config.AppType == "Web") { if (_authProvider.ConfClient == null) { return false; } var accounts = await _authProvider.ConfClient.GetAccountsAsync(); _authProvider.UserAccount = accounts.FirstOrDefault(); return null != _authProvider.UserAccount; } return false; } public async Task AddUserToTokenCache(string authorizationCode) { BaseConfig config = _configMonitor.CurrentValue; var result = await _authProvider.ConfClient .AcquireTokenByAuthorizationCode(config.AADSettings.Scopes, authorizationCode) .ExecuteAsync(); _authProvider.UserAccount = result.Account; return result.IdToken; } public async Task SignOut() { BaseConfig config = _configMonitor.CurrentValue; if (config.AppType == "Desktop") { if (_authProvider.UserAccount != null) { await _authProvider.PubClient.RemoveAsync(_authProvider.UserAccount); } } else if (config.AppType == "Web") { if (_authProvider.UserAccount != null) { await _authProvider.ConfClient.RemoveAsync(_authProvider.UserAccount); } } _authProvider.UserAccount = null; IsInitialized = false; GraphServiceClient = null; } public void RebuildClient() { _authProvider.Invalidate(); _appState.AadConfigComplete = _authProvider.RebuildMsalClients(); } } } ================================================ FILE: src/PresenceLight.Core/GraphServices/TokenCacheHelper.cs ================================================ using System; using System.IO; using System.Security.Cryptography; using Microsoft.Identity.Client; namespace PresenceLight.Core { static class TokenCacheHelper { public static void EnableSerialization(ITokenCache tokenCache) { tokenCache.SetBeforeAccess(BeforeAccessNotification); tokenCache.SetAfterAccess(AfterAccessNotification); } /// /// Path to the token cache. Note that this could be something different for instance for MSIX applications: /// private static readonly string CacheFolderPath = $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\PresenceLight\\"; private static readonly string CacheFileName = "msalcache.bin"; private static readonly object FileLock = new object(); private static void BeforeAccessNotification(TokenCacheNotificationArgs args) { lock (FileLock) { args.TokenCache.DeserializeMsalV3(File.Exists($"{CacheFolderPath}{CacheFileName}") ? ProtectedData.Unprotect(File.ReadAllBytes($"{CacheFolderPath}{CacheFileName}"), null, DataProtectionScope.CurrentUser) : null); } } private static void AfterAccessNotification(TokenCacheNotificationArgs args) { // if the access operation resulted in a cache update if (args.HasStateChanged) { lock (FileLock) { // reflect changesgs in the persistent store if (!Directory.Exists(CacheFolderPath)) { Directory.CreateDirectory(CacheFolderPath); } File.WriteAllBytes($"{CacheFolderPath}{CacheFileName}", ProtectedData.Protect(args.TokenCache.SerializeMsalV3(), null, DataProtectionScope.CurrentUser) ); } } } } } ================================================ FILE: src/PresenceLight.Core/Helpers.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace PresenceLight.Core { /// /// Represents the status of hours passed. /// public enum HoursPassedStatus { Off, Keep, White } /// /// Provides helper methods for various operations. /// public static class Helpers { public static bool AreStringsNotEmpty(string[] strings) { bool result = true; foreach (var s in strings) { result = result && !string.IsNullOrEmpty(s); } return result; } /// /// Opens the specified URL in the default browser. /// /// The URL to open. public static void OpenBrowser(string url) { // Opens request in the browser. try { System.Diagnostics.Process.Start(new ProcessStartInfo(url)); } catch { Console.WriteLine("Hello"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { url = url.Replace("&", "^&"); System.Diagnostics.Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true }); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { System.Diagnostics.Process.Start("xdg-open", url); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { System.Diagnostics.Process.Start("open", url); } else { throw; } } } /// /// Converts a camel case or pascal case string into a human-readable format by inserting spaces between words. /// /// The input string to be converted. /// The converted string with spaces between words. public static string HumanifyText(string text) { var r = new Regex(@" (?<=[A-Z])(?=[A-Z][a-z]) | (?<=[^A-Z])(?=[A-Z]) | (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace); return r.Replace(text, " "); } /// /// Converts the given HoursPassedStatus value to its corresponding string representation. /// /// The HoursPassedStatus value to convert. /// The string representation of the HoursPassedStatus value. public static string HoursPassedStatusString(HoursPassedStatus status) => status switch { HoursPassedStatus.Keep => "Keep", HoursPassedStatus.White => "White", HoursPassedStatus.Off => "Off", _ => throw new ArgumentException(message: "Invalid HoursPassedStatus Value", paramName: nameof(status)), }; /// /// Replaces the variables in the given body with the provided availability and activity. /// /// The body in which to replace the variables. /// The availability to replace the {{availability}} variable. /// The activity to replace the {{activity}} variable. /// The body with the variables replaced. public static string ReplaceVariables(string body, string? availability, string? activity) { if (body.Contains("{{availability}}")) { body = body.Replace("{{availability}}", availability ?? string.Empty); } if (body.Contains("{{activity}}")) { body = body.Replace("{{activity}}", activity ?? string.Empty); } return body; } } } ================================================ FILE: src/PresenceLight.Core/Lights/CustomApiService/CustomApiService.cs ================================================ using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace PresenceLight.Core { public interface ICustomApiService { Task SetColor(string availability, string? activity, CancellationToken cancellationToken = default); void Initialize(AppState _appState); } public class CustomApiService : ICustomApiService { private MediatR.IMediator _mediator; private string _currentAvailability = string.Empty; private string _currentActivity = string.Empty; HttpClient _client; private readonly ILogger _logger; private AppState _appState; public CustomApiService(AppState appState, ILogger logger, MediatR.IMediator mediator) { _logger = logger; _appState = appState; _mediator = mediator; _client = new HttpClient { Timeout = TimeSpan.FromSeconds(_appState.Config.LightSettings.CustomApi.CustomApiTimeout > 0 ? _appState.Config.LightSettings.CustomApi.CustomApiTimeout : 20) }; } public void Initialize(AppState appState) { _appState = appState; } public async Task SetColor(string availability, string? activity, CancellationToken cancellationToken = default) { string result = await SetAvailability(availability, cancellationToken); result += await SetActivity(activity, cancellationToken); return result; } private async Task CallCustomApiForActivityChanged(object sender, string newActivity, CancellationToken cancellationToken) { string method = string.Empty; string uri = string.Empty; string body = string.Empty; string result = string.Empty; switch (newActivity) { case "Available": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityAvailable.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityAvailable.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityAvailable.Body; break; case "Presenting": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityPresenting.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityPresenting.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityPresenting.Body; break; case "InACall": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityInACall.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityInACall.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityInACall.Body; break; case "InAConferenceCall": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityInAConferenceCall.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityInAConferenceCall.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityInAConferenceCall.Body; break; case "InAMeeting": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityInAMeeting.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityInAMeeting.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityInAMeeting.Body; break; case "Busy": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityBusy.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityBusy.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityBusy.Body; break; case "Away": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityAway.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityAway.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityAway.Body; break; case "BeRightBack": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityBeRightBack.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityBeRightBack.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityBeRightBack.Body; break; case "DoNotDisturb": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityDoNotDisturb.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityDoNotDisturb.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityDoNotDisturb.Body; break; case "Idle": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityIdle.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityIdle.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityIdle.Body; break; case "Offline": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityOffline.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityOffline.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityOffline.Body; break; case "Off": method = _appState.Config.LightSettings.CustomApi.CustomApiActivityOff.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiActivityOff.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiActivityOff.Body; break; default: break; } return await PerformWebRequest(method, uri, body, result, cancellationToken); } private async Task CallCustomApiForAvailabilityChanged(object sender, string newAvailability, CancellationToken cancellationToken) { string method = string.Empty; string uri = string.Empty; string body = string.Empty; string result = string.Empty; switch (newAvailability) { case "Available": method = _appState.Config.LightSettings.CustomApi.CustomApiAvailable.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiAvailable.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiAvailable.Body; break; case "Busy": method = _appState.Config.LightSettings.CustomApi.CustomApiBusy.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiBusy.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiBusy.Body; break; case "BeRightBack": method = _appState.Config.LightSettings.CustomApi.CustomApiBeRightBack.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiBeRightBack.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiBeRightBack.Body; break; case "Away": method = _appState.Config.LightSettings.CustomApi.CustomApiAway.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiAway.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiAway.Body; break; case "DoNotDisturb": method = _appState.Config.LightSettings.CustomApi.CustomApiDoNotDisturb.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiDoNotDisturb.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiDoNotDisturb.Body; break; case "AvailableIdle": method = _appState.Config.LightSettings.CustomApi.CustomApiAvailableIdle.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiAvailableIdle.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiAvailableIdle.Body; break; case "Offline": method = _appState.Config.LightSettings.CustomApi.CustomApiOffline.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiOffline.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiOffline.Body; break; case "Off": method = _appState.Config.LightSettings.CustomApi.CustomApiOff.Method; uri = _appState.Config.LightSettings.CustomApi.CustomApiOff.Uri; body = _appState.Config.LightSettings.CustomApi.CustomApiOff.Body; break; default: break; } return await PerformWebRequest(method, uri, body, result, cancellationToken); } private async Task SetAvailability(string availability, CancellationToken cancellationToken) { string result = string.Empty; if (availability != _currentAvailability) { result = await CallCustomApiForAvailabilityChanged(this, availability, cancellationToken); if (!cancellationToken.IsCancellationRequested) { _currentAvailability = availability; } else { // operation was cancelled } } else { // availability did not change: don't spam call the api } return result; } private async Task SetActivity(string activity, CancellationToken cancellationToken) { string result = string.Empty; if (activity != _currentActivity) { result = await CallCustomApiForActivityChanged(this, activity, cancellationToken); if (!cancellationToken.IsCancellationRequested) { _currentActivity = activity; } else { // operation was cancelled } } else { // activity did not change: don't spam call the api } return result; } static Stack _lastUriCalled = new Stack(1); private async Task PerformWebRequest(string method, string uri, string body, string result, CancellationToken cancellationToken) { if (_lastUriCalled.Contains($"{method}|{uri}")) { _logger.LogDebug("No Change to State... NOT calling Api"); return "Skipped"; } using (Serilog.Context.LogContext.PushProperty("method", method)) using (Serilog.Context.LogContext.PushProperty("uri", uri)) using (Serilog.Context.LogContext.PushProperty("body", body)) { if (Helpers.AreStringsNotEmpty(new string[] { method, uri })) { try { if (_appState.Config.LightSettings.CustomApi.IgnoreCertificateErrors) { var httpClientHandler = new HttpClientHandler(); httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }; _client = new HttpClient(httpClientHandler); } else { _client = new HttpClient(); } _client.Timeout = TimeSpan.FromSeconds(_appState.Config.LightSettings.CustomApi.CustomApiTimeout > 0 ? _appState.Config.LightSettings.CustomApi.CustomApiTimeout : 20); HttpResponseMessage response = new HttpResponseMessage(); if (_appState.Config.LightSettings.CustomApi.UseBasicAuth) { var byteArray = Encoding.ASCII.GetBytes($"{_appState.Config.LightSettings.CustomApi.BasicAuthUserName}:{_appState.Config.LightSettings.CustomApi.BasicAuthUserPassword}"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); } switch (method) { case "GET": response = await _client.GetAsync(uri, cancellationToken); break; case "POST": // check if body is empty if (string.IsNullOrEmpty(body)) { response = await _client.PostAsync(uri, null, cancellationToken); break; } else { // Replace any variables in the body // The following variables are supported: // {{availability}} - The current availability // {{activity}} - The current activity // Check if the body contains any variables using a regular expression and replace them body = Helpers.ReplaceVariables(body, _appState.Presence.Availability, _appState.Presence.Activity); var content = new StringContent(body, Encoding.UTF8, "application/json"); response = await _client.PostAsync(uri, content, cancellationToken); break; } } string responseBody = await response.Content.ReadAsStringAsync(cancellationToken); result = $"{(int)response.StatusCode} {response.StatusCode}: {responseBody}"; string message = $"Sending {method} method to {uri} with body {body}"; _logger.LogInformation(message); _lastUriCalled.TryPop(out string res); _lastUriCalled.Push($"{method}|{uri}|{body}"); using (Serilog.Context.LogContext.PushProperty("result", result)) _logger.LogDebug(message + " Results"); } catch (Exception e) { _logger.LogError(e, "Error Performing Web Request"); result = $"Error: {e.Message}"; } } return result; } } } } ================================================ FILE: src/PresenceLight.Core/Lights/CustomApiService/Initialize/InitializeCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.Initialize { public class InitializeCommand : IRequest { public AppState AppState { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/CustomApiService/Initialize/InitializeHandler.cs ================================================ using MediatR; using PresenceLight.Core; using System; using System.Threading; using System.Threading.Tasks; using YeelightAPI.Models; namespace PresenceLight.Core.Initialize { internal class InitializeHandler : IRequestHandler { readonly ICustomApiService _service; public InitializeHandler(ICustomApiService service) { _service = service; } Task IRequestHandler.Handle(InitializeCommand command, CancellationToken cancellationToken) { _service.Initialize(command.AppState); return Unit.Task; } } } ================================================ FILE: src/PresenceLight.Core/Lights/CustomApiService/SetColor/SetColorCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.CustomApiServices { public class SetColorCommand : IRequest { public string Availability { get; set; } public string Activity { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/CustomApiService/SetColor/SetColorHandler.cs ================================================ using MediatR; using PresenceLight.Core; using System; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.CustomApiServices { internal class SetColorHandler : IRequestHandler { readonly ICustomApiService _service; public SetColorHandler(ICustomApiService service) { _service = service; } public async Task Handle(SetColorCommand command, CancellationToken cancellationToken) { return await _service.SetColor(command.Availability, command.Activity, cancellationToken); } } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/FindBridge/FindBridgeCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.HueServices { public class FindBridgeCommand : IRequest { } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/FindBridge/FindBridgeHandler.cs ================================================ using MediatR; using System; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.HueServices { public class FindBridgeHandler : IRequestHandler { IHueService _service; public FindBridgeHandler(IHueService hueService) { _service = hueService; } public async Task Handle(FindBridgeCommand command, CancellationToken cancellationToken) { return await _service.FindBridge(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/GetGroups/GetGroupsCommand.cs ================================================ using HueApi.Models; using MediatR; using System.Collections.Generic; namespace PresenceLight.Core.HueServices { public class GetGroupsCommand : IRequest> { } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/GetGroups/GetGroupsHandler.cs ================================================ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using HueApi.Models; using MediatR; namespace PresenceLight.Core.HueServices { public class GetGroupsHandler : IRequestHandler> { IHueService _service; public GetGroupsHandler(IHueService hueService) { _service = hueService; } public async Task> Handle(GetGroupsCommand command, CancellationToken cancellationToken) { return await _service.GetGroups(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/GetLights/GetLightsCommand.cs ================================================ using MediatR; using HueApi.Models; using System.Collections.Generic; namespace PresenceLight.Core.HueServices { public class GetLightsCommand : IRequest> { } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/GetLights/GetLightsHandler.cs ================================================ using MediatR; using HueApi.Models; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.HueServices { public class GetLightsHandler : IRequestHandler> { IHueService _service; public GetLightsHandler(IHueService hueService) { _service = hueService; } public async Task> Handle(GetLightsCommand command, CancellationToken cancellationToken) { return await _service.GetLights(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/HueService.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using HueApi; using HueApi.BridgeLocator; using HueApi.ColorConverters.Original.Extensions; using HueApi.Models; using HueApi.Models.Requests; using Microsoft.Extensions.Logging; namespace PresenceLight.Core { public interface IHueService { Task SetColor(string availability, string activity, string lightId); Task RegisterBridge(); Task> GetLights(); Task> GetGroups(); Task FindBridge(); void Initialize(AppState appState); } public class HueService : IHueService { private AppState _appState; private LocalHueApi _client; private readonly ILogger _logger; public HueService(AppState appState, ILogger logger) { _logger = logger; _appState = appState; } public void Initialize(AppState appState) { _appState = appState; } public async Task SetColor(string availability, string activity, string lightId) { if (_appState.HueLights == null || _appState.HueLights.Count() == 0) { if (lightId.Contains("group_id:")) { _appState.SetHueLights(await GetGroups()); } else { _appState.SetHueLights(await GetLights()); } } if (string.IsNullOrEmpty(lightId)) { _logger.LogInformation("Selected Hue Light Not Specified"); return; } try { _client = new LocalHueApi(_appState.Config.LightSettings.Hue.HueIpAddress, _appState.Config.LightSettings.Hue.HueApiKey); var o = await Handle(_appState.Config.LightSettings.Hue.UseActivityStatus ? activity : availability, lightId); if (o.returnFunc) { return; } var color = o.color.Replace("#", ""); var command = o.command; var message = ""; switch (color.Length) { case var length when color.Length == 6: // Do Nothing break; case var length when color.Length > 6: // Get last 6 characters color = color.Substring(0, 6); break; default: throw new ArgumentException("Supplied Color had an issue"); } var rgbColor = new HueApi.ColorConverters.RGBColor(color); // Set the color using extension method command.SetColor(rgbColor); if (availability == "Off") { command.TurnOff(); if (lightId.Contains("group_id:")) { var groupCommand = new UpdateGroupedLight(); groupCommand.TurnOff(); await _client.UpdateGroupedLightAsync(Guid.Parse(lightId.Replace("group_id:", "")), groupCommand); } else { await _client.UpdateLightAsync(Guid.Parse(lightId.Replace("id:", "")), command); } message = $"Turning Hue Light {lightId} Off"; _logger.LogInformation(message); return; } if (_appState.Config.LightSettings.UseDefaultBrightness) { if (_appState.Config.LightSettings.DefaultBrightness == 0) { command.TurnOff(); } else { command.TurnOn(); command.Dimming = new Dimming { Brightness = Convert.ToDouble(_appState.Config.LightSettings.DefaultBrightness) }; command.Dynamics = new Dynamics { Duration = 0 }; } } else { if (_appState.Config.LightSettings.Hue.Brightness == 0) { command.TurnOff(); } else { command.TurnOn(); command.Dimming = new Dimming { Brightness = Convert.ToDouble(_appState.Config.LightSettings.Hue.Brightness) }; command.Dynamics = new Dynamics { Duration = 0 }; } } if (lightId.Contains("group_id:")) { var newLightId = ((GroupedLight)_appState.HueLights.First(a => ((GroupedLight)a).IdV1 == lightId.Replace("group_id:", ""))).Id; var groupCommand = new UpdateGroupedLight(); groupCommand.Color = command.Color; groupCommand.On = command.On; groupCommand.Dimming = command.Dimming; groupCommand.Dynamics = command.Dynamics; await _client.GroupedLight.UpdateAsync(newLightId, groupCommand); } else { var newLightId = ((Light)_appState.HueLights.First(a => ((Light)a).IdV1 == lightId.Replace("id:", ""))).Id; await _client.Light.UpdateAsync(newLightId, command); } message = $"Setting Hue Light {lightId} to {color}"; _logger.LogInformation(message); } catch (Exception e) { _logger.LogError(e, "Error Occurred Setting Color"); throw; } } //Need to wire up a way to do this without user intervention public async Task RegisterBridge() { if (string.IsNullOrEmpty(_appState.Config.LightSettings.Hue.HueApiKey)) { try { _logger.LogInformation("Registering with Hue Bridge - Please press the button on your bridge"); var result = await LocalHueApi.RegisterAsync(_appState.Config.LightSettings.Hue.HueIpAddress, "PresenceLight", Environment.MachineName, true); return result.Username; // RegisterAsync returns RegisterEntertainmentResult with Username property } catch (Exception e) { _logger.LogError(e, "Error Occurred Registering Bridge"); return String.Empty; } } return _appState.Config.LightSettings.Hue.HueApiKey; } public async Task FindBridge() { try { HttpBridgeLocator locator = new HttpBridgeLocator(); var bridges = await locator.LocateBridgesAsync(TimeSpan.FromSeconds(5)); if (bridges.Any()) { return bridges.FirstOrDefault().IpAddress; } } catch (Exception e) { _logger.LogError(e, "Error Occurred Finding Bridge"); return String.Empty; } return String.Empty; } public async Task> GetLights() { try { if (_client == null) { _client = new LocalHueApi(_appState.Config.LightSettings.Hue.HueIpAddress, _appState.Config.LightSettings.Hue.HueApiKey); } var lightsResponse = await _client.Light.GetAllAsync(); return lightsResponse.Data; } catch (Exception e) { _logger.LogError(e, message: "Error Occurred Getting Lights"); throw; } } public async Task> GetGroups() { try { if (_client == null) { _client = new LocalHueApi(_appState.Config.LightSettings.Hue.HueIpAddress, _appState.Config.LightSettings.Hue.HueApiKey); } var groupsResponse = await _client.GroupedLight.GetAllAsync(); return groupsResponse.Data; } catch (Exception e) { _logger.LogError(e, "Error Occurred Getting Groups"); throw; } } private async Task<(string color, UpdateLight command, bool returnFunc)> Handle(string presence, string lightId) { var props = _appState.Config.LightSettings.Hue.Statuses.GetType().GetProperties().ToList(); if (_appState.Config.LightSettings.Hue.UseActivityStatus) { props = props.Where(a => a.Name.ToLower().StartsWith("activity")).ToList(); } else { props = props.Where(a => a.Name.ToLower().StartsWith("availability")).ToList(); } string color = ""; string message; var command = new UpdateLight(); if (presence.Contains('#')) { // provided presence is actually a custom color color = presence; command.TurnOn(); return (color, command, false); } foreach (var prop in props) { if (presence == prop.Name.Replace("Status", "").Replace("Availability", "").Replace("Activity", "")) { var value = (AvailabilityStatus)prop.GetValue(_appState.Config.LightSettings.Hue.Statuses); if (!value.Disabled) { command.TurnOn(); color = value.Color; return (color, command, false); } else { command.TurnOff(); if (lightId.Contains("group_id:")) { var groupCommand = new UpdateGroupedLight(); groupCommand.TurnOff(); await _client.UpdateGroupedLightAsync(Guid.Parse(lightId.Replace("group_id:", "")), groupCommand); } else { await _client.UpdateLightAsync(Guid.Parse(lightId.Replace("id:", "")), command); } message = $"Turning Hue Light {lightId} Off"; _logger.LogInformation(message); return (color, command, true); } } } return (color, command, false); } } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/Initialize/InitializeCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.HueServices { public class InitializeCommand : IRequest { public AppState AppState { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/Initialize/InitializeHandler.cs ================================================ using MediatR; using PresenceLight.Core; using System; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.HueServices { internal class InitializeHandler : IRequestHandler { IHueService _service; public InitializeHandler(IHueService hueService) { _service = hueService; } async Task IRequestHandler.Handle(InitializeCommand command, CancellationToken cancellationToken) { _service.Initialize(command.AppState); await Task.CompletedTask; } } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/RegisterBridge/RegisterBridgeCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.HueServices { public class RegisterBridgeCommand : IRequest { } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/RegisterBridge/RegisterBridgeHandler.cs ================================================ using MediatR; using System; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.HueServices { public class RegisterBridgeHandler : IRequestHandler { IHueService _service; public RegisterBridgeHandler(IHueService hueService) { _service = hueService; } public async Task Handle(RegisterBridgeCommand command, CancellationToken cancellationToken) { return await _service.RegisterBridge(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/SetColor/SetColorCommand.cs ================================================ using MediatR; using System.Threading.Tasks; namespace PresenceLight.Core.HueServices { public class SetColorCommand : IRequest { public string Availability { get; set; } public string Activity { get; set; } public string LightID { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/HueServices/SetColor/SetColorHandler.cs ================================================ using MediatR; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.HueServices.HueService { public class SetColorHandler : IRequestHandler { IHueService _service; public SetColorHandler(IHueService hueService) { _service = hueService; } public async Task Handle(SetColorCommand command, CancellationToken cancellationToken) { await _service.SetColor(command.Availability, command.Activity, command.LightID); return default; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LifxServices/GetAllGroups/GetAllGroupsCommand.cs ================================================ using LifxCloud.NET.Models; using MediatR; using System.Collections.Generic; namespace PresenceLight.Core.LifxServices { public class GetAllGroupsCommand : IRequest> { public string ApiKey { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LifxServices/GetAllGroups/GetAllGroupsHandler.cs ================================================ using LifxCloud.NET; using LifxCloud.NET.Models; using MediatR; using Microsoft.Extensions.Logging; using PresenceLight.Core; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.LifxServices { internal class GetAllGroupsHandler : IRequestHandler> { LIFXService _service; public GetAllGroupsHandler(LIFXService service) { _service = service; } public async Task> Handle(GetAllGroupsCommand command, CancellationToken cancellationToken) { return await _service.GetAllGroups(command.ApiKey); } } } ================================================ FILE: src/PresenceLight.Core/Lights/LifxServices/GetAllLights/GetAllLightsCommand.cs ================================================ using LifxCloud.NET.Models; using MediatR; using System.Collections.Generic; namespace PresenceLight.Core.LifxServices { public class GetAllLightsCommand : IRequest> { public string ApiKey { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LifxServices/GetAllLights/GetAllLightsHandler.cs ================================================ using LifxCloud.NET; using LifxCloud.NET.Models; using MediatR; using Microsoft.Extensions.Logging; using PresenceLight.Core; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.LifxServices { internal class GetAllLightsHandler : IRequestHandler> { LIFXService _service; public GetAllLightsHandler(LIFXService service) { _service = service; } public async Task> Handle(GetAllLightsCommand command, CancellationToken cancellationToken) { return await _service.GetAllLights(command.ApiKey); } } } ================================================ FILE: src/PresenceLight.Core/Lights/LifxServices/Initialize/InitializeCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.LifxServices { public class InitializeCommand : IRequest { public AppState AppState { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LifxServices/Initialize/InitializeHandler.cs ================================================ using MediatR; using PresenceLight.Core; using System; using System.Threading; using System.Threading.Tasks; using YeelightAPI.Models; namespace PresenceLight.Core.LifxServices { internal class InitializeHandler : IRequestHandler { LIFXService _service; public InitializeHandler(LIFXService service) { _service = service; } async Task IRequestHandler.Handle(InitializeCommand command, CancellationToken cancellationToken) { _service.Initialize(command.AppState); await Task.CompletedTask; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LifxServices/LIFXOAuthHelper.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Web; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; namespace PresenceLight.Core { public class LIFXOAuthHelper { private const string LIFXAuthority = "https://cloud.lifx.com/oauth"; private readonly string _lIFXTokenEndpoint = $"{LIFXAuthority}/token"; private readonly string _lIFXAuthorizationEndpoint = $"{LIFXAuthority}/authorize"; private readonly AppState _appState; public LIFXOAuthHelper(AppState appState) { _appState = appState; } public async Task InitiateTokenRetrieval() { var builder = WebApplication.CreateBuilder(); builder.Logging.ClearProviders(); var app = builder.Build(); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); app.Run(async ctx => { Task WriteResponse(HttpContext ctx) { ctx.Response.StatusCode = 200; ctx.Response.ContentType = "text/html"; return ctx.Response.WriteAsync("Please return to the app.", Encoding.UTF8); } switch (ctx.Request.Method) { case "GET": await WriteResponse(ctx); tcs.TrySetResult(ctx.Request.QueryString.Value); break; case "POST" when !ctx.Request.HasFormContentType: ctx.Response.StatusCode = 415; break; case "POST": { using var sr = new StreamReader(ctx.Request.Body, Encoding.UTF8); var body = await sr.ReadToEndAsync(); await WriteResponse(ctx); tcs.TrySetResult(body); break; } default: ctx.Response.StatusCode = 405; break; } }); var browserPort = 17236; app.Urls.Add($"http://localhost:{browserPort}/"); app.Start(); var timeout = TimeSpan.FromMinutes(5); string redirectUri = string.Format($"http://localhost:{browserPort}/"); string state = RandomDataBase64Url(32); string authorizationRequest = string.Format("{0}?response_type=code&scope=remote_control:all&client_id={1}&state={2}&redirect_uri={3}", _lIFXAuthorizationEndpoint, _appState.Config.LightSettings.LIFX.LIFXClientId, state, HttpUtility.UrlEncode(redirectUri) ); Helpers.OpenBrowser(authorizationRequest); var qs = await tcs.Task.WaitAsync(timeout); var qsDict = QueryHelpers.ParseQuery(qs.Replace("?", "")); StringValues code; qsDict.TryGetValue("code", out code); await app.DisposeAsync(); var formContent = new FormUrlEncodedContent(new[] { new KeyValuePair("code", code.ToString()), new KeyValuePair("client_id", _appState.Config.LightSettings.LIFX.LIFXClientId), new KeyValuePair("client_secret", _appState.Config.LightSettings.LIFX.LIFXClientSecret), new KeyValuePair("grant_type", "authorization_code") }); // sends the request HttpClient _client = new HttpClient(); _client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); var response = await _client.PostAsync(_lIFXTokenEndpoint, formContent); string responseText = await response.Content.ReadAsStringAsync(); Dictionary tokenEndpointDecoded = JsonConvert.DeserializeObject>(responseText); string _accessToken = tokenEndpointDecoded["access_token"]; return _accessToken; } /// /// Returns URI-safe data with a given input length. /// /// Input length (nb. output will be longer) /// private static string RandomDataBase64Url(uint length) { var rng = RandomNumberGenerator.Create(); byte[] bytes = new byte[length]; rng.GetBytes(bytes); return Base64UrlEncodeNoPadding(bytes); } /// /// Base64url no-padding encodes the given input buffer. /// /// /// private static string Base64UrlEncodeNoPadding(byte[] buffer) { string base64 = Convert.ToBase64String(buffer); // Converts base64 to base64url. base64 = base64.Replace("+", "-", StringComparison.OrdinalIgnoreCase); base64 = base64.Replace("/", "_", StringComparison.OrdinalIgnoreCase); // Strips padding. base64 = base64.Replace("=", "", StringComparison.OrdinalIgnoreCase); return base64; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LifxServices/LifxService.cs ================================================ using System; using LifxCloud.NET; using LifxCloud.NET.Models; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using System.Linq; namespace PresenceLight.Core { public class LIFXService { private AppState _appState; private LifxCloudClient _client; private readonly ILogger _logger; MediatR.IMediator _mediator; public LIFXService(AppState appState, MediatR.IMediator mediator, ILogger logger) { _appState = appState; _logger = logger; _mediator = mediator; } public void Initialize(AppState appState) { _appState = appState; } public async Task> GetAllLights(string apiKey = null) { try { if (!string.IsNullOrEmpty(apiKey)) { _appState.Config.LightSettings.LIFX.LIFXApiKey = apiKey; } if (!_appState.Config.LightSettings.LIFX.IsEnabled || string.IsNullOrEmpty(_appState.Config.LightSettings.LIFX.LIFXApiKey)) { return new List(); } _client = await LifxCloudClient.CreateAsync(_appState.Config.LightSettings.LIFX.LIFXApiKey); return await _client.ListLights(Selector.All); } catch (Exception e) { _logger.LogError(e, "Error Getting Lights"); throw; } } public async Task> GetAllGroups(string apiKey = null) { try { if (!string.IsNullOrEmpty(apiKey)) { _appState.Config.LightSettings.LIFX.LIFXApiKey = apiKey; } if (!_appState.Config.LightSettings.LIFX.IsEnabled || string.IsNullOrEmpty(_appState.Config.LightSettings.LIFX.LIFXApiKey)) { return new List(); } _client = await LifxCloudClient.CreateAsync(_appState.Config.LightSettings.LIFX.LIFXApiKey); return await _client.ListGroups(Selector.All); } catch (Exception e) { _logger.LogError(e, "Error Getting Groups"); throw; } } public async Task SetColor(string availability, string activity, string lightId, string apiKey = null) { if (string.IsNullOrEmpty(lightId)) { _logger.LogInformation("Selected LIFX Light Not Specified"); return; } Selector selector = null; if (!lightId.Contains("group")) { selector = new Selector.LightId(lightId.Replace("id:", "")); } else { selector = new Selector.GroupId(lightId.Replace("group_id:", "")); } if (!string.IsNullOrEmpty(apiKey)) { _appState.Config.LightSettings.LIFX.LIFXApiKey = apiKey; } if (!_appState.Config.LightSettings.LIFX.IsEnabled || string.IsNullOrEmpty(_appState.Config.LightSettings.LIFX.LIFXApiKey)) { return; } try { _client = await LifxCloudClient.CreateAsync(_appState.Config.LightSettings.LIFX.LIFXApiKey); var o = await Handle(_appState.Config.LightSettings.LIFX.UseActivityStatus ? activity : availability, lightId); if (o.returnFunc) { return; } var color = o.color.Replace("#", ""); var command = o.command; var message = ""; switch (color.Length) { case var length when color.Length == 6: // Do Nothing break; case var length when color.Length > 6: // Get last 6 characters color = color.Substring(0,6); break; default: throw new ArgumentException("Supplied Color had an issue"); } if (availability == "Off") { _logger.LogInformation($"Turning LIFX Light {lightId} Off - LIFXService:SetColor"); command.Power = PowerState.Off; var result = await _client.SetState(selector, command); return; } if (_appState.Config.LightSettings.UseDefaultBrightness) { if (_appState.Config.LightSettings.DefaultBrightness == 0) { command.Power = PowerState.Off; } else { command.Power = PowerState.On; command.Brightness = Convert.ToDouble(_appState.Config.LightSettings.DefaultBrightness) / 100; command.Duration = 0; } } else { if (_appState.Config.LightSettings.LIFX.Brightness == 0) { command.Power = PowerState.Off; } else { command.Power = PowerState.On; command.Brightness = Convert.ToDouble(_appState.Config.LightSettings.DefaultBrightness) / 100; command.Duration = 0; } } command.Color = color; await _client.SetState(selector, command); message = $"Setting LIFX Light {lightId} to {color}"; _logger.LogInformation(message); } catch (Exception e) { _logger.LogError(e, "Error Occurred Setting Color"); throw; } } private async Task<(string color, SetStateRequest command, bool returnFunc)> Handle(string presence, string lightId) { var props = _appState.Config.LightSettings.LIFX.Statuses.GetType().GetProperties().ToList(); if (_appState.Config.LightSettings.LIFX.UseActivityStatus) { props = props.Where(a => a.Name.ToLower().StartsWith("activity")).ToList(); } else { props = props.Where(a => a.Name.ToLower().StartsWith("availability")).ToList(); } string color = ""; string message; var command = new SetStateRequest(); if (presence.Contains("#")) { // provided presence is actually a custom color color = presence; command.Power = PowerState.On; return (color, command, false); } foreach (var prop in props) { if (presence == prop.Name.Replace("Status", "").Replace("Availability", "").Replace("Activity", "")) { var value = (AvailabilityStatus)prop.GetValue(_appState.Config.LightSettings.LIFX.Statuses); if (!value.Disabled) { command.Power = PowerState.On; color = value.Color; return (color, command, false); } else { command.Power = PowerState.Off; Selector selector = null; if (!lightId.Contains("group")) { selector = new Selector.LightId(lightId.Replace("id:", "")); } else { selector = new Selector.GroupId(lightId.Replace("group_id:", "")); } await _client.SetState(selector, command); message = $"Turning LIFX Light {lightId} Off"; _logger.LogInformation(message); return (color, command, true); } } } return (color, command, false); } } } ================================================ FILE: src/PresenceLight.Core/Lights/LifxServices/SetColor/SetColorCommand.cs ================================================ using MediatR; using System.Threading.Tasks; namespace PresenceLight.Core.LifxServices { public class SetColorCommand : IRequest { public string Availability { get; set; } public string Activity { get; set; } public string? LightId { get; set; } public string ApiKey { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LifxServices/SetColor/SetColorHandler.cs ================================================ using LifxCloud.NET; using MediatR; using Microsoft.Extensions.Logging; using PresenceLight.Core; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.LifxServices { internal class SetColorHandler : IRequestHandler { LIFXService _service; public SetColorHandler(LIFXService service) { _service = service; } public async Task Handle(SetColorCommand command, CancellationToken cancellationToken) { await _service.SetColor(command.Availability, command.Activity, command.LightId, command.ApiKey); return default; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LocalSerialHostService/GetSerialHosts/GetSerialHostsCommand.cs ================================================ using MediatR; using System.Collections.Generic; namespace PresenceLight.Core.LocalSerialHostServices { public class GetPortCommand : IRequest> { } } ================================================ FILE: src/PresenceLight.Core/Lights/LocalSerialHostService/GetSerialHosts/GetSerialHostsHandler.cs ================================================ using MediatR; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.LocalSerialHostServices { internal class GetAvailablePortsHandler : IRequestHandler> { ILocalSerialHostService _service; public GetAvailablePortsHandler(ILocalSerialHostService service) { _service = service; } public async Task> Handle(GetPortCommand command, CancellationToken cancellationToken) { return await _service.GetPorts(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/LocalSerialHostService/Initialize/InitializeCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.LocalSerialHostServices { public class InitializeCommand : IRequest { public AppState AppState { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LocalSerialHostService/Initialize/InitializeHandler.cs ================================================ using MediatR; using PresenceLight.Core; using System; using System.Threading; using System.Threading.Tasks; using YeelightAPI.Models; namespace PresenceLight.Core.LocalSerialHostServices { internal class InitializeHandler : IRequestHandler { readonly ILocalSerialHostService _service; public InitializeHandler(ILocalSerialHostService service) { _service = service; } Task IRequestHandler.Handle(InitializeCommand command, CancellationToken cancellationToken) { _service.Initialize(command.AppState); return Unit.Task; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LocalSerialHostService/LocalSerialHost.cs ================================================ using System; using System.Collections.Generic; using System.IO.Ports; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace PresenceLight.Core { public interface ILocalSerialHostService { Task SetColor(string availability, string? activity, CancellationToken cancellationToken = default); Task> GetPorts(); void Initialize(AppState _appState); } public class LocalSerialHostService : ILocalSerialHostService { private MediatR.IMediator _mediator; private string _currentAvailability = string.Empty; private string _currentActivity = string.Empty; private SerialPort _port = null; private string _lineEnding = ""; private readonly ILogger _logger; private AppState _appState; private readonly object serialWriteLock = new object(); public LocalSerialHostService(AppState appState, ILogger logger, MediatR.IMediator mediator) { _logger = logger; _appState = appState; _mediator = mediator; switch (appState.Config.LightSettings.LocalSerialHost.LocalSerialHostMainSetup.LineEnding) { case "CR" : _lineEnding = "\r"; break; case "LF" : _lineEnding = "\n"; break; case "CRLF" : _lineEnding = "\r\n"; break; default : _logger.LogDebug("Line endings not set or empty string"); _lineEnding = ""; break; } if (!string.IsNullOrEmpty(appState.Config.LightSettings.LocalSerialHost.LocalSerialHostMainSetup.Port)) { SetupSerialPort(appState.Config.LightSettings.LocalSerialHost.LocalSerialHostMainSetup.Port); } } ~LocalSerialHostService() { if (_port != null && _port.IsOpen) { _port.Close(); } } public void Initialize(AppState appState) { _appState = appState; if (_port != null && _port.IsOpen) { _port.Close(); _port = null; } switch (appState.Config.LightSettings.LocalSerialHost.LocalSerialHostMainSetup.LineEnding) { case "CR" : _lineEnding = "\r"; break; case "LF" : _lineEnding = "\n"; break; case "CRLF" : _lineEnding = "\r\n"; break; default : _logger.LogDebug("Line endings not set or empty string"); _lineEnding = ""; break; } if (!string.IsNullOrEmpty(appState.Config.LightSettings.LocalSerialHost.LocalSerialHostMainSetup.Port)) { SetupSerialPort(appState.Config.LightSettings.LocalSerialHost.LocalSerialHostMainSetup.Port); } } public async Task SetColor(string availability, string? activity, CancellationToken cancellationToken = default) { string result = await SetAvailability(availability, cancellationToken); result += await SetActivity(activity, cancellationToken); return result; } private async Task CallLocalSerialHostForActivityChanged(object sender, string newActivity, CancellationToken cancellationToken) { string message = string.Empty; string result = string.Empty; switch (newActivity) { case "Available": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityAvailable; break; case "Presenting": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityPresenting; break; case "InACall": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityInACall; break; case "InAConferenceCall": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityInAConferenceCall; break; case "InAMeeting": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityInAMeeting; break; case "Busy": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityBusy; break; case "Away": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityAway; break; case "BeRightBack": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityBeRightBack; break; case "DoNotDisturb": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityDoNotDisturb; break; case "Idle": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityIdle; break; case "Offline": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityOffline; break; case "Off": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostActivityOff; break; default: break; } return await PerformSerialMessage(message, result, cancellationToken); } private async Task CallLocalSerialHostForAvailabilityChanged(object sender, string newAvailability, CancellationToken cancellationToken) { string message = string.Empty; string result = string.Empty; switch (newAvailability) { case "Available": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostAvailable; break; case "Busy": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostBusy; break; case "BeRightBack": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostBeRightBack; break; case "Away": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostAway; break; case "DoNotDisturb": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostDoNotDisturb; break; case "AvailableIdle": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostAvailableIdle; break; case "Offline": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostOffline; break; case "Off": message = _appState.Config.LightSettings.LocalSerialHost.LocalSerialHostOff; break; default: break; } return await PerformSerialMessage(message, result, cancellationToken); } private async Task SetAvailability(string availability, CancellationToken cancellationToken) { string result = string.Empty; if (availability != _currentAvailability) { result = await CallLocalSerialHostForAvailabilityChanged(this, availability, cancellationToken); if (!cancellationToken.IsCancellationRequested) { _currentAvailability = availability; } else { // operation was cancelled } } else { // availability did not change: don't spam call the api } return result; } private async Task SetActivity(string activity, CancellationToken cancellationToken) { string result = string.Empty; if (activity != _currentActivity) { result = await CallLocalSerialHostForActivityChanged(this, activity, cancellationToken); if (!cancellationToken.IsCancellationRequested) { _currentActivity = activity; } else { // operation was cancelled } } else { // activity did not change: don't spam call the api } return result; } static Stack _lastLineEndingCalled = new Stack(1); private async Task PerformSerialMessage(string serialMessage, string result, CancellationToken cancellationToken) { if (_lastLineEndingCalled.Contains($"{serialMessage}")) { _logger.LogDebug("No Change to State... NOT calling Api"); return "Skipped"; } using (Serilog.Context.LogContext.PushProperty("message", serialMessage)) { if (!string.IsNullOrEmpty(serialMessage)) { try { if (_port == null || !_port.IsOpen) { _logger.LogWarning("Serial Port not setup in PerformSerialMessage. Attempting to initialize"); SetupSerialPort(_appState.Config.LightSettings.LocalSerialHost.LocalSerialHostMainSetup.Port); } Task writeTask = Task.Run(() => { string writeResult = ""; try { lock (serialWriteLock) { _port.Write(serialMessage + _lineEnding); writeResult = _port.ReadLine(); } } catch (Exception e) { _logger.LogError(e, "Error Performing Serial Write"); writeResult = $"Error: {e.Message}"; } return writeResult.Trim(); }); string message = $"Sending {serialMessage} to {_port.PortName}"; result = await Task.WhenAny(writeTask).Result; _logger.LogInformation(message); _lastLineEndingCalled.TryPop(out string res); _lastLineEndingCalled.Push($"{serialMessage}"); using (Serilog.Context.LogContext.PushProperty("result", result)) _logger.LogDebug(message + " Results"); } catch (Exception e) { _logger.LogError(e, "Error Performing Web Request"); result = $"Error: {e.Message}"; } } return result; } } public async Task> GetPorts() { try { IEnumerable ports = await Task.Run(() => SerialPort.GetPortNames()); return ports; } catch (Exception e) { _logger.LogError(e, "Unable to retrieve serial ports"); throw; } } private void SetupSerialPort(string serialPort) { if (!string.IsNullOrEmpty(serialPort) && serialPort != "null") { _port = new SerialPort(); _port.PortName = serialPort; _port.ReadTimeout = 500; _port.WriteTimeout = 500; int baudRateInternal = 0; if (int.TryParse(_appState.Config.LightSettings.LocalSerialHost.LocalSerialHostMainSetup.BaudRate, out baudRateInternal)) { _port.BaudRate = baudRateInternal; } else { _port.BaudRate = 9600; } if (!_port.IsOpen) { _port.Open(); } } } } } ================================================ FILE: src/PresenceLight.Core/Lights/LocalSerialHostService/SetColor/SetColorCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.LocalSerialHostServices { public class SetColorCommand : IRequest { public string Availability { get; set; } public string Activity { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/LocalSerialHostService/SetColor/SetColorHandler.cs ================================================ using MediatR; using PresenceLight.Core; using System; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.LocalSerialHostServices { internal class SetColorHandler : IRequestHandler { readonly ILocalSerialHostService _service; public SetColorHandler(ILocalSerialHostService service) { _service = service; } public async Task Handle(SetColorCommand command, CancellationToken cancellationToken) { return await _service.SetColor(command.Availability, command.Activity, cancellationToken); } } } ================================================ FILE: src/PresenceLight.Core/Lights/RemoteHueServices/GetGroups/GetGroupsCommand.cs ================================================ using MediatR; using HueApi.Models; using System.Collections.Generic; namespace PresenceLight.Core.RemoteHueServices { public class GetGroupsCommand : IRequest> { } } ================================================ FILE: src/PresenceLight.Core/Lights/RemoteHueServices/GetGroups/GetGroupsHandler.cs ================================================ using MediatR; using HueApi.Models; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.RemoteHueServices { internal class GetGroupsHandler : IRequestHandler> { readonly IRemoteHueService _service; public GetGroupsHandler(IRemoteHueService service) { _service = service; } public async Task> Handle(GetGroupsCommand command, CancellationToken cancellationToken) { return await _service.GetGroups(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/RemoteHueServices/GetLights/GetLightsCommand.cs ================================================ using MediatR; using HueApi.Models; using System.Collections.Generic; namespace PresenceLight.Core.RemoteHueServices { public class GetLightsCommand : IRequest> { } } ================================================ FILE: src/PresenceLight.Core/Lights/RemoteHueServices/GetLights/GetLightsHandler.cs ================================================ using MediatR; using HueApi.Models; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.RemoteHueServices { internal class GetLightsHandler : IRequestHandler> { readonly IRemoteHueService _service; public GetLightsHandler(IRemoteHueService service) { _service = service; } public async Task> Handle(GetLightsCommand command, CancellationToken cancellationToken) { return await _service.GetLights(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/RemoteHueServices/RegisterBridge/RegisterBridgeCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.RemoteHueServices { public class RegisterBridgeCommand : IRequest<(string bridgeId, string apiKey, string bridgeIp)> { } } ================================================ FILE: src/PresenceLight.Core/Lights/RemoteHueServices/RegisterBridge/RegisterBridgeHandler.cs ================================================ using MediatR; using System; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.RemoteHueServices { internal class RegisterBridgeHandler : IRequestHandler { readonly IRemoteHueService _service; public RegisterBridgeHandler(IRemoteHueService service) { _service = service; } public async Task<(string bridgeId, string apiKey, string bridgeIp)> Handle(RegisterBridgeCommand command, CancellationToken cancellationToken) { return await _service.RegisterBridge(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/RemoteHueServices/RemoteAuthenticationClient.cs ================================================ using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using JeffWilcox.Utilities.Silverlight; using Newtonsoft.Json; using Q42.HueApi.Interfaces; using Q42.HueApi.Models; namespace Q42.HueApi.Models { public class AccessTokenResponseV2 { public DateTimeOffset CreatedDate { get; set; } public string access_token { get; set; } public int expires_in { get; set; } public string refresh_token { get; set; } public string scope { get; set; } public string token_type { get; set; } public AccessTokenResponseV2() { CreatedDate = DateTimeOffset.UtcNow; } public DateTimeOffset AccessTokenExpireTime() { return CreatedDate.AddSeconds(expires_in); } } } namespace Q42.HueApi.Interfaces { public interface IRemoteAuthenticationClient { Uri BuildAuthorizeUri(string state, string deviceId, string? deviceName = null, string responseType = "code"); RemoteAuthorizeResponse ProcessAuthorizeResponse(string responseData); /// /// Initialize with existing AccessTokenResponse /// /// void Initialize(AccessTokenResponseV2 storedAccessToken); Task GetToken(string code); Task RefreshToken(string refreshToken); /// /// Gets a valid access token /// /// Task GetValidToken(); } } namespace Q42.HueApi { /// /// https://developers.meethue.com/develop/hue-api/remote-authentication-oauth/ /// public class RemoteAuthenticationClientV2 : IRemoteAuthenticationClient { public bool IsInitialized { get; protected set; } private readonly string _clientId; private readonly string _clientSecret; private readonly string _appId; private AccessTokenResponseV2? _lastAuthorizationResponse; private HttpClient _httpClient; /// /// /// /// Identifies the client that is making the request. The value passed in this parameter must exactly match the value you receive from hue. Note that the underscore is not used in the clientid name of this parameter. /// The clientsecret you have received from Hue when registering for the Hue Remote API. /// Identifies the app that is making the request. The value passed in this parameter must exactly match the value you receive from hue. public RemoteAuthenticationClientV2(string clientId, string clientSecret, string appId) { if (string.IsNullOrEmpty(clientId)) throw new ArgumentNullException(nameof(clientId)); if (string.IsNullOrEmpty(clientSecret)) throw new ArgumentNullException(nameof(clientSecret)); if (string.IsNullOrEmpty(appId)) throw new ArgumentNullException(nameof(appId)); _clientId = clientId; _clientSecret = clientSecret; _appId = appId; _httpClient = new HttpClient(); } public void Initialize(AccessTokenResponseV2 accessTokenResponse) { IsInitialized = true; _lastAuthorizationResponse = accessTokenResponse; } /// /// Authorization request /// /// Provides any state that might be useful to your application upon receipt of the response. The Hue Authorization Server roundtrips this parameter, so your application receives the same value it sent. To mitigate against cross-site request forgery (CSRF), it is strongly recommended to include an anti-forgery token in the state, and confirm it in the response. One good choice for a state token is a string of 30 or so characters constructed using a high-quality random-number generator. /// The device identifier must be a unique identifier for the app or device accessing the Hue Remote API. /// The device name should be the name of the app or device accessing the remote API. The devicename is used in the user's "My Apps" overview in the Hue Account (visualized as: " on "). If not present, deviceid is also used for devicename. The is the application name you provided to us the moment you requested access to the remote API. /// The response_type value must be "code". /// public Uri BuildAuthorizeUri(string state, string deviceId, string? deviceName = null, string responseType = "code") { if (string.IsNullOrEmpty(responseType)) throw new ArgumentNullException(nameof(responseType)); string url = string.Format("https://api.meethue.com/v2/oauth2/authorize?client_id={0}&response_type={5}&state={1}&appid={3}&deviceid={2}&devicename={4}", _clientId, state, deviceId, _appId, deviceName, responseType); return new Uri(url); } public RemoteAuthorizeResponse ProcessAuthorizeResponse(string responseData) { string url = responseData; string[] parts = url.Split(new char[] { '?', '&' }); RemoteAuthorizeResponse result = new RemoteAuthorizeResponse(); foreach (var part in parts) { string[] nv = part.Split(new char[] { '=' }); if (nv.Length == 2) { if (nv[0].ToLower() == "code") result.Code = nv[1]; if (nv[0].ToLower() == "state") result.State = nv[1]; } } return result; } /// /// Get an access token /// /// Code retreived using ProcessAuthorizeResponse /// public async Task GetToken(string code) { var requestUri = new Uri($"https://api.meethue.com/v2/oauth2/token"); var formParameters = new Dictionary { {"code", code}, {"grant_type", "authorization_code"} }; var formContent = new FormUrlEncodedContent(formParameters); //Do a token request var responseTask = await _httpClient.PostAsync(requestUri, formContent).ConfigureAwait(false); var responseString = responseTask.Headers.WwwAuthenticate.ToString(); responseString = responseString.Replace("Digest ", string.Empty); string nonce = GetNonce(responseString); if (!string.IsNullOrEmpty(nonce)) { //Get token var request = new HttpRequestMessage() { RequestUri = requestUri, Method = HttpMethod.Post, Content = formContent }; //Build request var credentials = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{_clientId}:{_clientSecret}")); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials); var accessTokenResponse = await _httpClient.SendAsync(request).ConfigureAwait(false); var accessTokenResponseString = await accessTokenResponse.Content.ReadAsStringAsync().ConfigureAwait(false); var accessToken = JsonConvert.DeserializeObject(accessTokenResponseString); _lastAuthorizationResponse = accessToken; return accessToken; } return null; } private static string GetNonce(string r) { //Find the nonce int startNonce = r.IndexOf("nonce=") + 7; int endNonce = r.IndexOf("\"", startNonce); string nonce = r.Substring(startNonce, endNonce - startNonce); return nonce; } public async Task RefreshToken(string refreshToken) { CheckInitialized(); var requestUri = new Uri("https://api.meethue.com/v2/oauth2/token"); var formParameters = new Dictionary { {"refresh_token", refreshToken}, {"grant_type", "refresh_token"} }; var formContent = new FormUrlEncodedContent(formParameters); //Do a token request var responseTask = await _httpClient.PostAsync(requestUri, formContent).ConfigureAwait(false); var responseString = responseTask.Headers.WwwAuthenticate.ToString(); responseString = responseString.Replace("Digest ", string.Empty); string nonce = GetNonce(responseString); if (!string.IsNullOrEmpty(nonce)) { //Get token var request = new HttpRequestMessage() { RequestUri = requestUri, Method = HttpMethod.Post, Content = formContent }; //Build request var credentials = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{_clientId}:{_clientSecret}")); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials); var accessTokenResponse = await _httpClient.SendAsync(request).ConfigureAwait(false); var accessTokenResponseString = await accessTokenResponse.Content.ReadAsStringAsync().ConfigureAwait(false); var accessToken = JsonConvert.DeserializeObject(accessTokenResponseString); _lastAuthorizationResponse = accessToken; return accessToken; } return null; } /// /// Calculate hash for token request /// /// /// /// /// private static string CalculateHash(string clientId, string clientSecret, string nonce, string path) { var HASH1 = MD5.GetMd5String($"{clientId}:oauth2_client@api.meethue.com:{clientSecret}"); var HASH2 = MD5.GetMd5String("POST:" + path); var response = MD5.GetMd5String(HASH1 + ":" + nonce + ":" + HASH2); return response; } /// /// Refreshes the token if needed /// /// public async Task GetValidToken() { CheckInitialized(); if (_lastAuthorizationResponse != null) { if (_lastAuthorizationResponse.AccessTokenExpireTime() > DateTimeOffset.UtcNow.AddMinutes(-5)) { return _lastAuthorizationResponse.access_token; } else { var newToken = await this.RefreshToken(_lastAuthorizationResponse.refresh_token).ConfigureAwait(false); return newToken?.access_token; } } throw new HueException("Unable to get access token. Access token and Refresh token expired."); } /// /// Check if the RemoteAuthenticationClient is initialized /// private void CheckInitialized() { if (!IsInitialized) throw new InvalidOperationException("RemoteAuthenticationClient is not initialized. First call Initialize."); } } } ================================================ FILE: src/PresenceLight.Core/Lights/RemoteHueServices/RemoteHueService.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Q42.HueApi; using Q42.HueApi.ColorConverters; using Q42.HueApi.ColorConverters.Original; using Q42.HueApi.Interfaces; using Q42.HueApi.Models; namespace PresenceLight.Core { public interface IRemoteHueService { Task SetColor(string availability, string activity, string lightId, string bridgeId); Task<(string bridgeId, string apiKey, string bridgeIp)> RegisterBridge(); Task> GetLights(); Task> GetGroups(); } public class RemoteHueService : IRemoteHueService { private AppState _appState; private RemoteHueClient _client; private IRemoteAuthenticationClient _authClient; private readonly ILogger _logger; private MediatR.IMediator _mediator; private string _cacheFile = $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\PresenceLight\\huetoken.cache"; public RemoteHueService(AppState appState, ILogger logger, MediatR.IMediator mediator) { _mediator = mediator; _logger = logger; _appState = appState; CreateAuthClient(); } private void CreateAuthClient() { if (Helpers.AreStringsNotEmpty(new string[] { _appState.Config.LightSettings.Hue.RemoteHueClientId, _appState.Config.LightSettings.Hue.RemoteHueClientSecret, _appState.Config.LightSettings.Hue.RemoteHueClientAppName })) { _authClient = new RemoteAuthenticationClientV2(_appState.Config.LightSettings.Hue.RemoteHueClientId, _appState.Config.LightSettings.Hue.RemoteHueClientSecret, _appState.Config.LightSettings.Hue.RemoteHueClientAppName); } else { _logger.LogWarning("Remote Hue Not Configured Properly in Config"); } } private async Task InitializeClient() { if (Helpers.AreStringsNotEmpty(new string[] { _appState.Config.LightSettings.Hue.RemoteBridgeId, _appState.Config.LightSettings.Hue.HueApiKey })) { if (_authClient == null) { CreateAuthClient(); } if (_client == null || !_client.IsInitialized) { try { var token = await _authClient.GetValidToken(); } catch { if (File.Exists(_cacheFile)) { AccessTokenResponseV2 response = JsonConvert.DeserializeObject(File.ReadAllText(_cacheFile)); if (response != null) { _authClient.Initialize(response); } } else { // prompt auth await RegisterBridge(); } } _client = new RemoteHueClient(_authClient.GetValidToken); _client.Initialize(_appState.Config.LightSettings.Hue.RemoteBridgeId, _appState.Config.LightSettings.Hue.HueApiKey); } } } public async Task<(string bridgeId, string apiKey, string bridgeIp)> RegisterBridge() { try { await GetAccessToken(); var bridges = await _client.GetBridgesAsync(); string bridgeId; string bridgeIp; if (string.IsNullOrEmpty(_appState.Config.LightSettings.Hue.RemoteBridgeId)) { bridgeId = bridges.First().Id; bridgeIp = bridges.First().InternalIpaddress; } else { bridgeId = _appState.Config.LightSettings.Hue.RemoteBridgeId; bridgeIp = _appState.Config.LightSettings.Hue.HueIpAddress; } string apiKey; if (string.IsNullOrEmpty(_appState.Config.LightSettings.Hue.HueApiKey)) { apiKey = await _client.RegisterAsync(bridgeId, _appState.Config.LightSettings.Hue.RemoteHueClientAppName); } else { apiKey = _appState.Config.LightSettings.Hue.HueApiKey; } if (!_client.IsInitialized) { _client.Initialize(bridgeId, apiKey); } //Register app return (bridgeId, apiKey, bridgeIp); } catch (Exception e) { _logger.LogError(e, "Error Occurred Registering Remote Bridge"); throw; } } public async Task SetColor(string availability, string activity, string lightId, string bridgeId) { await InitializeClient(); if (_client == null || !_client.IsInitialized) { _logger.LogInformation("Hue Client Not Initialized"); return; } try { if (string.IsNullOrEmpty(lightId)) { _logger.LogInformation("Selected Hue Light Not Specified"); return; } var o = await Handle(_appState.Config.LightSettings.Hue.UseActivityStatus ? activity : availability, lightId); if (o.returnFunc) { return; } var color = o.color.Replace("#", ""); var command = o.command; var message = ""; switch (color.Length) { case var length when color.Length == 6: // Do Nothing break; case var length when color.Length > 6: // Get last 6 characters color = color.Substring(0, 6); break; default: throw new ArgumentException("Supplied Color had an issue"); } command.SetColor(new RGBColor(color)); if (availability == "Off") { command.On = false; if (lightId.Contains("group_id:")) { await _client.SendGroupCommandAsync(command, lightId.Replace("group_id:", "")); } else { await _client.SendCommandAsync(command, new List { lightId.Replace("id:", "") }); } message = $"Turning Hue Light {lightId} Off"; _logger.LogInformation(message); return; } if (_appState.Config.LightSettings.UseDefaultBrightness) { if (_appState.Config.LightSettings.DefaultBrightness == 0) { command.On = false; } else { command.On = true; command.Brightness = Convert.ToByte(((Convert.ToDouble(_appState.Config.LightSettings.DefaultBrightness) / 100) * 254)); command.TransitionTime = new TimeSpan(0); } } else { if (_appState.Config.LightSettings.Hue.Brightness == 0) { command.On = false; } else { command.On = true; command.Brightness = Convert.ToByte(((Convert.ToDouble(_appState.Config.LightSettings.Hue.Brightness) / 100) * 254)); command.TransitionTime = new TimeSpan(0); } } if (lightId.Contains("group_id:")) { await _client.SendGroupCommandAsync(command, lightId.Replace("group_id:", "")); } else { await _client.SendCommandAsync(command, new List { lightId.Replace("id:", "") }); } message = $"Setting Hue Light {lightId} to {color}"; _logger.LogInformation(message); } catch (Exception e) { _logger.LogError(e, "Error Occurred Setting Color"); throw; } } public async Task> GetLights() { await InitializeClient(); if (_client == null || !_client.IsInitialized) { _logger.LogInformation("Hue Client Not Initialized"); return null; } try { var q42Lights = await _client.GetLightsAsync(); // if there are no lights, get some if (!q42Lights.Any()) { await _client.SearchNewLightsAsync(); Thread.Sleep(40000); q42Lights = await _client.GetNewLightsAsync(); } // Convert Q42 Light models to HueApi Light models var hueApiLights = new List(); foreach (var light in q42Lights) { var guid = light.UniqueId?.Replace(":", "-"); hueApiLights.Add(new HueApi.Models.Light { IdV1 = light.Id, Metadata = new HueApi.Models.Metadata { Name = light.Name } }); } return hueApiLights; } catch (Exception e) { _logger.LogError(e, "Error Getting Lights"); throw; } } public async Task> GetGroups() { await InitializeClient(); if (_client == null || !_client.IsInitialized) { _logger.LogInformation("Hue Client Not Initialized"); return null; } try { var q42Groups = await _client.GetGroupsAsync(); // Convert Q42 Group models to HueApi GroupedLight models var hueApiGroups = new List(); foreach (var group in q42Groups) { hueApiGroups.Add(new HueApi.Models.GroupedLight { IdV1 = group.Id, Metadata = new HueApi.Models.Metadata { Name = group.Name } }); } return hueApiGroups; } catch (Exception e) { _logger.LogError(e, "Error Getting Groups"); throw; } } public Task FindBridge() { throw new NotImplementedException(); } private async Task<(string color, LightCommand command, bool returnFunc)> Handle(string presence, string lightId) { var props = _appState.Config.LightSettings.Hue.Statuses.GetType().GetProperties().ToList(); if (_appState.Config.LightSettings.Hue.UseActivityStatus) { props = props.Where(a => a.Name.ToLower().StartsWith("activity")).ToList(); } else { props = props.Where(a => a.Name.ToLower().StartsWith("availability")).ToList(); } string color = ""; string message; var command = new LightCommand(); if (presence.Contains("#")) { // provided presence is actually a custom color color = presence; command.On = true; return (color, command, false); } foreach (var prop in props) { if (presence == prop.Name.Replace("Status", "").Replace("Availability", "").Replace("Activity", "")) { var value = (AvailabilityStatus)prop.GetValue(_appState.Config.LightSettings.Hue.Statuses); if (!value.Disabled) { command.On = true; color = value.Color; return (color, command, false); } else { command.On = false; if (lightId.Contains("group_id:")) { await _client.SendGroupCommandAsync(command, lightId.Replace("group_id:", "")); } else { await _client.SendCommandAsync(command, new List { lightId.Replace("id:", "") }); } message = $"Turning Hue Light {lightId} Off"; _logger.LogInformation(message); return (color, command, true); } } } return (color, command, false); } private async Task GetAccessToken() { try { Uri authorizeUri = _authClient.BuildAuthorizeUri(_appState.Config.LightSettings.Hue.RemoteHueClientAppName, _appState.Config.LightSettings.Hue.RemoteHueClientAppName); TryBindListenerOnFreePort(out HttpListener http, out int port, out string redirectURI); Helpers.OpenBrowser(authorizeUri.ToString()); // Waits for the OAuth authorization response. var context = await http.GetContextAsync(); //Sends an HTTP response to the browser. var response = context.Response; string responseString = string.Format("Please return to the app."); var buffer = System.Text.Encoding.UTF8.GetBytes(responseString); response.ContentLength64 = buffer.Length; var responseOutput = response.OutputStream; Task responseTask = responseOutput.WriteAsync(buffer, 0, buffer.Length).ContinueWith((task) => { responseOutput.Close(); http.Stop(); Debug.WriteLine("HTTP server stopped."); }); // extracts the code var code = context.Request.QueryString.Get("code") ?? ""; var incoming_state = context.Request.QueryString.Get("state"); var accessToken = await _authClient.GetToken(code); if (accessToken != null) { _authClient.Initialize(accessToken); if (File.Exists(_cacheFile)) { File.Delete(_cacheFile); } await File.WriteAllTextAsync(_cacheFile, JsonConvert.SerializeObject(accessToken)); } _client = new RemoteHueClient(_authClient.GetValidToken); } catch (Exception e) { _logger.LogError(e, "Error Occurred Processing Access Token for Remote Bridge"); throw; } } private static bool TryBindListenerOnFreePort(out HttpListener httpListener, out int port, out string uri) { // IANA suggested range for dynamic or private ports const int MinPort = 49215; const int MaxPort = 65535; for (port = MinPort; port < MaxPort; port++) { httpListener = new HttpListener(); uri = $"http://localhost:{port}/"; httpListener.Prefixes.Add(uri); try { httpListener.Start(); return true; } catch { // nothing to do here -- the listener disposes itself when Start throws } } port = 0; uri = null; httpListener = null; return false; } } } ================================================ FILE: src/PresenceLight.Core/Lights/RemoteHueServices/SetColor/SetColorCommand.cs ================================================ using MediatR; using System.Threading.Tasks; namespace PresenceLight.Core.RemoteHueServices { public class SetColorCommand : IRequest { public string Availability { get; set; } public string Activity { get; set; } public string LightId { get; set; } public string BridgeId { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/RemoteHueServices/SetColor/SetColorHandler.cs ================================================ using MediatR; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.RemoteHueServices { internal class SetColorHandler : IRequestHandler { readonly IRemoteHueService _service; public SetColorHandler(IRemoteHueService service) { _service = service; } public async Task Handle(SetColorCommand command, CancellationToken cancellationToken) { await _service.SetColor(command.Availability, command.Activity, command.LightId, command.BridgeId); return default; } } } ================================================ FILE: src/PresenceLight.Core/Lights/ServicesExtensions.cs ================================================ using Microsoft.Extensions.DependencyInjection; namespace PresenceLight.Core { public static class ServicesExtensions { public static void AddPresenceServices(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/WizServices/GetLights/GetLightCommand.cs ================================================ using System.Collections.Generic; using MediatR; using OpenWiz; namespace PresenceLight.Core.WizServices { public class GetLightCommand : IRequest { } } ================================================ FILE: src/PresenceLight.Core/Lights/WizServices/GetLights/GetLightHandler.cs ================================================ using MediatR; using OpenWiz; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.WizServices { public class GetLightHandler : IRequestHandler { IWizService _service; public GetLightHandler(IWizService service) { _service = service; } public async Task Handle(GetLightCommand command, CancellationToken cancellationToken) { return await _service.GetLight(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/WizServices/IPAddressExtensions.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace PresenceLight.Core.Lights.WizServices { internal static class IPAddressExtensions { /// /// Check if given IPv4 is a link-local (auto configuration) address (according to RFC3927) /// /// https://tools.ietf.org/html/rfc3927 /// The IPv4 /// True if link-local, false otherwise public static bool IsIPv4LinkLocal(this IPAddress ip) { if (ip.AddressFamily != AddressFamily.InterNetwork) { // Not an IPv4, simply return false return false; } return ip.ToString().StartsWith("169.254."); } /// /// Check if given IP is a loopback /// This is just a helper extension around the static method IPAddress.IsLoopback /// /// The IP /// True if loopback, False otherwise public static bool IsLoopback(this IPAddress ip) { return IPAddress.IsLoopback(ip); } /// /// Check if given IPv4 is in private range (according to RFC1918) /// /// https://tools.ietf.org/html/rfc1918 /// The IPv4 /// True if private, false otherwise public static bool IsIPv4Private(this IPAddress ip) { if (ip.AddressFamily != AddressFamily.InterNetwork) { // Not an IPv4, simply return false return false; } byte[] bytes = ip.GetAddressBytes(); switch (bytes[0]) { // 10.0.0.0 - 10.255.255.255 (10/8 prefix) case 10: return true; // 172.16.0.0 - 172.31.255.255 (172.16/12 prefix) case 172: return bytes[1] < 32 && bytes[1] >= 16; // 192.168.0.0 - 192.168.255.255 (192.168/16 prefix) case 192: return bytes[1] == 168; // Others default: return false; } } /// /// Get a list of all IPv4 addresses in a specified network /// /// If IP or mask not IPv4 /// Any IP from the network /// The network mask (subnet) /// A list of IPAddress public static IPAddress GetNetworkLoopbackAddress(this IPAddress ip, IPAddress mask) { if (ip.AddressFamily != AddressFamily.InterNetwork) { throw new ArgumentException("Not an IPv4 address", nameof(ip)); } if (mask.AddressFamily != AddressFamily.InterNetwork) { throw new ArgumentException("Not an IPv4 address", nameof(mask)); } List range = new List(); byte[] maskBytes = mask.GetAddressBytes(); byte[] ipBytes = ip.GetAddressBytes(); // Start IP (network IP) = IP AND MASK byte[] startIpBytes = Enumerable.Range(0, 4) .Select(i => (byte)(ipBytes[i] & maskBytes[i])) .ToArray(); // Last IP (broadcast IP) = IP OR NOT MASK byte[] endIpBytes = Enumerable.Range(0, 4) .Select(i => (byte)(ipBytes[i] | ~maskBytes[i])) .ToArray(); if (!Enumerable.Range(0, 4).Any(i => startIpBytes[i] > endIpBytes[i])) { for (int b0 = startIpBytes[0]; b0 <= endIpBytes[0]; b0++) { for (int b1 = startIpBytes[1]; b1 <= endIpBytes[1]; b1++) { for (int b2 = startIpBytes[2]; b2 <= endIpBytes[2]; b2++) { for (int b3 = startIpBytes[3]; b3 <= endIpBytes[3]; b3++) { range.Add(new IPAddress(new byte[] { (byte)b0, (byte)b1, (byte)b2, (byte)b3 })); } } } } } else { // Something went wrong : a start byte is above an end byte and thus will lead to bad results } return range.Last(); } } internal static class NetworkInterfaceExtensions { /// /// Retrieve the list of all first private IPv4 of each interfaces that are up /// /// List of private IPv4 information public static UnicastIPAddressInformation GetAllUpNetworkInterfacesFirstPrivateIPv4() { return NetworkInterface.GetAllNetworkInterfaces() // Keep only connected interfaces .Where(itf => itf.OperationalStatus == OperationalStatus.Up) // Retrieve all unicast addresses of each interface .SelectMany(itf => itf.GetIPProperties().UnicastAddresses) // Keep only private IPv4 .Where(info => !info.Address.IsLoopback() && !info.Address.IsIPv4LinkLocal() && info.Address.IsIPv4Private() && info.PrefixOrigin == PrefixOrigin.Dhcp) .FirstOrDefault(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/WizServices/SetColor/SetColorCommand.cs ================================================ using MediatR; using System.Threading.Tasks; namespace PresenceLight.Core.WizServices { public class SetColorCommand : IRequest { public string Availability { get; set; } public string Activity { get; set; } public string LightID { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/WizServices/SetColor/SetColorHandler.cs ================================================ using MediatR; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.WizServices.WizService { public class SetColorHandler : IRequestHandler { IWizService _service; public SetColorHandler(IWizService hueService) { _service = hueService; } public async Task Handle(SetColorCommand command, CancellationToken cancellationToken) { await _service.SetColor(command.Availability, command.Activity, command.LightID); return default; } } } ================================================ FILE: src/PresenceLight.Core/Lights/WizServices/WizLight.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace PresenceLight.Core.WizServices { public class WizLight { public string LightName { get; set; } public string MacAddress { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/WizServices/WizService.cs ================================================ using System; using System.Linq; using System.Net; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OpenWiz; using PresenceLight.Core.WizServices; using HueApi.ColorConverters; namespace PresenceLight.Core { public interface IWizService { Task GetLight(); Task SetColor(string availability, string activity, string lightId); } public class WizService : IWizService { private AppState _appState; private readonly ILogger _logger; public WizService(AppState appState, ILogger logger) { _logger = logger; _appState = appState; } public WizService(AppState appState) { _appState = appState; } public async Task GetLight() { WizSocket socket = new WizSocket(); socket.GetSocket().EnableBroadcast = true; // This will enable sending to the broadcast address WizHandle handle = new WizHandle("000000000000", IPAddress.Parse(_appState.Config.LightSettings.Wiz.IPAddress)); // MAC doesn't matter here WizState state = WizState.MakeGetSystemConfig(); socket.GetSocket().ReceiveTimeout = 10000; // This will prevent the demo from running indefinitely await Task.Run(() => socket.SendTo(state, handle)); WizLight light = new WizLight(); // You won't easily get an IP address here, but this will list all Home IDs on the network. while (true) { try { state = socket.ReceiveFrom(handle); if (!string.IsNullOrEmpty(state.Result.ModuleName) && !string.IsNullOrEmpty(state.Result.Mac)) { Console.WriteLine($"Home ID for light {state.Result.Mac} = {state.Result.HomeId}"); light.MacAddress = state.Result.Mac; light.LightName = state.Result.ModuleName; } break; } catch (Exception e) { } } return light; } public async Task SetColor(string availability, string activity, string lightId) { if (string.IsNullOrEmpty(lightId)) { _logger.LogInformation("Selected Wiz Light Not Specified"); return; } try { var o = await Handle(_appState.Config.LightSettings.Wiz.UseActivityStatus ? activity : availability, lightId); if (o.returnFunc) { return; } var color = o.color.Replace("#", ""); var command = o.command; var message = ""; switch (color.Length) { case var length when color.Length == 6: // Do Nothing break; case var length when color.Length > 6: // Get last 6 characters color = color.Substring(0, 6); break; default: throw new ArgumentException("Supplied Color had an issue"); } var rgb = new RGBColor(color); command.R = Convert.ToInt32(rgb.R); command.B = Convert.ToInt32(rgb.B); command.G = Convert.ToInt32(rgb.G); if (availability == "Off") { command.State = false; await UpdateLight(command, lightId); message = $"Turning Wiz Light {lightId} Off"; _logger.LogInformation(message); return; } if (_appState.Config.LightSettings.UseDefaultBrightness) { if (_appState.Config.LightSettings.DefaultBrightness == 0) { command.State = false; } else { command.State = true; command.Dimming = _appState.Config.LightSettings.DefaultBrightness; command.Speed = 0; } } else { if (_appState.Config.LightSettings.Wiz.Brightness == 0) { command.State = false; } else { command.State = true; command.Dimming = _appState.Config.LightSettings.Wiz.Brightness; command.Speed = 0; } } await UpdateLight(command, lightId); message = $"Setting Wiz Light {lightId} to {color}"; _logger.LogInformation(message); } catch (Exception e) { _logger.LogError(e, "Error Occurred Setting Color"); throw; } } private async Task<(string color, WizParams command, bool returnFunc)> Handle(string presence, string lightId) { var props = _appState.Config.LightSettings.Wiz.Statuses.GetType().GetProperties().ToList(); if (_appState.Config.LightSettings.Wiz.UseActivityStatus) { props = props.Where(a => a.Name.ToLower().StartsWith("activity")).ToList(); } else { props = props.Where(a => a.Name.ToLower().StartsWith("availability")).ToList(); } string color = ""; string message; var command = new WizParams(); if (presence.Contains('#')) { // provided presence is actually a custom color color = presence; command.State = true; return (color, command, false); } foreach (var prop in props) { if (presence == prop.Name.Replace("Status", "").Replace("Availability", "").Replace("Activity", "")) { var value = (AvailabilityStatus)prop.GetValue(_appState.Config.LightSettings.Wiz.Statuses); if (!value.Disabled) { command.State = true; color = value.Color; return (color, command, false); } else { command.State = false; await UpdateLight(command, lightId); message = $"Turning Wiz Light {lightId} Off"; _logger.LogInformation(message); return (color, command, true); } } } return (color, command, false); } private async Task UpdateLight(WizParams wizParams, string lightId) { WizSocket socket = new WizSocket(); socket.GetSocket().EnableBroadcast = true; // This will enable sending to the broadcast address socket.GetSocket().ReceiveTimeout = 1000; // This will prevent the demo from running indefinitely WizHandle handle = new WizHandle(lightId, IPAddress.Parse(_appState.Config.LightSettings.Wiz.IPAddress)); // MAC doesn't matter here WizState state = new WizState { Method = WizMethod.setPilot, Params = wizParams }; await Task.Run(() => socket.SendTo(state, handle)); WizResult pilot; while (true) { state = socket.ReceiveFrom(handle); pilot = state.Result; break; } return pilot; } } } ================================================ FILE: src/PresenceLight.Core/Lights/WorkingHoursServices/IsInWorkingHours/IsInWorkingHoursCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.WorkingHoursServices { public class IsInWorkingHoursCommand : IRequest { } } ================================================ FILE: src/PresenceLight.Core/Lights/WorkingHoursServices/IsInWorkingHours/IsInWorkingHoursHandler.cs ================================================ using MediatR; using System; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.WorkingHoursServices { internal class IsInWorkingHoursHandler : IRequestHandler { IWorkingHoursService _service; public IsInWorkingHoursHandler(IWorkingHoursService service) { _service = service; } public async Task Handle(IsInWorkingHoursCommand command, CancellationToken cancellationToken) { return await Task.FromResult(_service.IsInWorkingHours()); } } } ================================================ FILE: src/PresenceLight.Core/Lights/WorkingHoursServices/UseWorkingHours/UseWorkingHoursCommand.cs ================================================ using MediatR; using System; namespace PresenceLight.Core.WorkingHoursServices { public class UseWorkingHoursCommand : IRequest { } } ================================================ FILE: src/PresenceLight.Core/Lights/WorkingHoursServices/UseWorkingHours/UseWorkingHoursHandler.cs ================================================ using MediatR; using System; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.WorkingHoursServices { internal class UseWorkingHoursHandler : IRequestHandler { IWorkingHoursService _service; public UseWorkingHoursHandler(IWorkingHoursService service) { _service = service; } public async Task Handle(UseWorkingHoursCommand command, CancellationToken cancellationToken) { return await Task.FromResult(_service.UseWorkingHours()); } } } ================================================ FILE: src/PresenceLight.Core/Lights/WorkingHoursServices/WorkingHoursService.cs ================================================ using System; using System.Globalization; namespace PresenceLight.Core { public interface IWorkingHoursService { public bool UseWorkingHours(); public bool IsInWorkingHours(); } public class WorkingHoursService : IWorkingHoursService { private readonly AppState _appState; public WorkingHoursService(AppState appState) { _appState = appState; } /// /// Exposes a config value should you want to short circuit the working hours test. /// public bool UseWorkingHours() { return _appState.Config.LightSettings.UseWorkingHours; } public bool IsInWorkingHours() { bool IsWorkingHours = false; if (!Helpers.AreStringsNotEmpty(new string[] {_appState.Config.LightSettings.WorkingHoursStartTime, _appState.Config.LightSettings.WorkingHoursEndTime, _appState.Config.LightSettings.WorkingDays})) { IsWorkingHours = false; return false; } if (!_appState.Config.LightSettings.WorkingDays.Contains(DateTime.Now.DayOfWeek.ToString(), StringComparison.OrdinalIgnoreCase)) { IsWorkingHours = false; return false; } // convert datetime to a TimeSpan bool validStart = TimeSpan.TryParse(_appState.Config.LightSettings.WorkingHoursStartTime, out TimeSpan start); bool validEnd = TimeSpan.TryParse(_appState.Config.LightSettings.WorkingHoursEndTime, out TimeSpan end); if (!validEnd || !validStart) { IsWorkingHours = false; return false; } TimeSpan now = DateTime.Now.TimeOfDay; // see if start comes before end if (start < end) { IsWorkingHours = start <= now && now <= end; return IsWorkingHours; } // start is after end, so do the inverse comparison IsWorkingHours = !(end < now && now < start); return IsWorkingHours; } } } ================================================ FILE: src/PresenceLight.Core/Lights/YeelightServices/FindLights/FindLightsCommand.cs ================================================ using MediatR; using YeelightAPI; namespace PresenceLight.Core.YeelightServices { public class GetLightCommand : IRequest { } } ================================================ FILE: src/PresenceLight.Core/Lights/YeelightServices/FindLights/FindLightsHandler.cs ================================================ using MediatR; using System.Threading; using System.Threading.Tasks; using YeelightAPI; namespace PresenceLight.Core.YeelightServices { internal class GetLightsHandler : IRequestHandler { IYeelightService _service; public GetLightsHandler(IYeelightService service) { _service = service; } public async Task Handle(GetLightCommand command, CancellationToken cancellationToken) { return await _service.FindLights(); } } } ================================================ FILE: src/PresenceLight.Core/Lights/YeelightServices/SetColor/SetColorCommand.cs ================================================ using MediatR; using System.Threading.Tasks; namespace PresenceLight.Core.YeelightServices { public class SetColorCommand : IRequest { public string Availability { get; set; } public string Activity { get; set; } public string LightId { get; set; } } } ================================================ FILE: src/PresenceLight.Core/Lights/YeelightServices/SetColor/SetColorHandler.cs ================================================ using MediatR; using System.Threading; using System.Threading.Tasks; namespace PresenceLight.Core.YeelightServices { internal class SetColorHandler : IRequestHandler { IYeelightService _service; public SetColorHandler(IYeelightService service) { _service = service; } public async Task Handle(SetColorCommand command, CancellationToken cancellationToken) { await _service.SetColor(command.Availability, command.Activity, command.LightId); return default; } } } ================================================ FILE: src/PresenceLight.Core/Lights/YeelightServices/YeelightService.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using YeelightAPI; using HueApi.ColorConverters; using Microsoft.Extensions.Logging; namespace PresenceLight.Core { public interface IYeelightService { Task SetColor(string availability, string activity, string lightId); Task FindLights(); } public class YeelightService : IYeelightService { private AppState _appState; private MediatR.IMediator _mediator; private DeviceGroup deviceGroup; private readonly ILogger _logger; public YeelightService(AppState appState, ILogger logger, MediatR.IMediator mediator) { _logger = logger; _appState = appState; _mediator = mediator; } public void Initialize(AppState appState) { _appState = appState; } public async Task SetColor(string availability, string activity, string lightId) { string message = ""; if (string.IsNullOrEmpty(lightId)) { _logger.LogInformation("Selected Yeelight Light Not Specified"); return; } var o = await Handle(_appState.Config.LightSettings.Yeelight.UseActivityStatus ? activity : availability, lightId); if (o.returnFunc) { return; } if (o.device == null) { message = $"Yeelight Device {lightId} Not Found"; _logger.LogError(message); throw new ArgumentOutOfRangeException(nameof(lightId), message); } o.device.OnNotificationReceived += Device_OnNotificationReceived; o.device.OnError += Device_OnError; if (!await o.device.Connect()) { message = $"Unable to Connect to Yeelight Device {lightId}"; _logger.LogError(message); throw new ArgumentOutOfRangeException(nameof(lightId), message); } try { var color = o.color.Replace("#", ""); switch (color.Length) { case var length when color.Length == 6: // Do Nothing break; case var length when color.Length > 6: // Get last 6 characters color = color.Substring(0, 6); break; default: throw new ArgumentException("Supplied Color had an issue"); } if (availability == "Off") { await o.device.TurnOff(); message = $"Turning Yeelight Light {lightId} Off"; _logger.LogInformation(message); return; } if (_appState.Config.LightSettings.UseDefaultBrightness) { if (_appState.Config.LightSettings.DefaultBrightness == 0) { await o.device.TurnOff(); } else { await o.device.TurnOn(); await o.device.SetBrightness(Convert.ToInt32(_appState.Config.LightSettings.DefaultBrightness)); } } else { if (_appState.Config.LightSettings.Hue.Brightness == 0) { await o.device.TurnOff(); } else { await o.device.TurnOn(); await o.device.SetBrightness(Convert.ToInt32(_appState.Config.LightSettings.Yeelight.Brightness)); } } var rgb = new RGBColor(color); await o.device.SetRGBColor((int)rgb.R, (int)rgb.G, (int)rgb.B); return; } catch (Exception e) { _logger.LogError(e, "Error Occurred Setting Color"); throw; } } private void Device_OnError(object sender, UnhandledExceptionEventArgs e) { //throw new NotImplementedException(); } private void Device_OnNotificationReceived(object sender, NotificationReceivedEventArgs e) { //throw new NotImplementedException(); } public async Task FindLights() { try { IEnumerable devices = await DeviceLocator.DiscoverAsync(); this.deviceGroup = new DeviceGroup(devices); return this.deviceGroup; } catch (Exception e) { _logger.LogError(e, "Error Occurred Finding Lights"); throw; } } private async Task<(string color, Device device, bool returnFunc)> Handle(string presence, string lightId) { var props = _appState.Config.LightSettings.Yeelight.Statuses.GetType().GetProperties().ToList(); if (_appState.Config.LightSettings.Yeelight.UseActivityStatus) { props = props.Where(a => a.Name.ToLower().StartsWith("activity")).ToList(); } else { props = props.Where(a => a.Name.ToLower().StartsWith("availability")).ToList(); } string color = ""; string message; var device = this.deviceGroup.FirstOrDefault(x => x.Id == lightId); if (device != null) { device.OnNotificationReceived += Device_OnNotificationReceived; device.OnError += Device_OnError; await device.Connect(); if (presence.Contains("#")) { // provided presence is actually a custom color color = presence; await device.TurnOn(); return (color, device, false); } foreach (var prop in props) { if (presence == prop.Name.Replace("Status", "").Replace("Availability", "").Replace("Activity", "")) { var value = (AvailabilityStatus)prop.GetValue(_appState.Config.LightSettings.Yeelight.Statuses); if (!value.Disabled) { await device.TurnOn(); color = value.Color; return (color, device, false); } else { await device.TurnOff(); message = $"Turning Yeelight Light {lightId} Off"; _logger.LogInformation(message); return (color, device, true); } } } } return (color, device, false); } } } ================================================ FILE: src/PresenceLight.Core/Logging/ILoggerExtensions.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; /// /// This class is purposefully put without a namespace so that it overrides the existing ILogger extensions /// to allow the additional context properties and log messaging to be written to the logs. /// public static class ILoggerExtensions { /// /// Formats and writes a log message at the specified log level. /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// Entry will be written on this level. /// The event id associated with the log. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.Log(logLevel, eventId, exception, message, args); } } /// /// Formats and writes a log message at the specified log level. /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// Entry will be written on this level. /// The event id associated with the log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.Log(logLevel, eventId, message, args); } } /// /// Formats and writes a log message at the specified log level. /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// Entry will be written on this level. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void Log(this ILogger logger, LogLevel logLevel, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.Log(logLevel, exception, message, args); } } /// /// Formats and writes a log message at the specified log level. /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// Entry will be written on this level. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void Log(this ILogger logger, LogLevel logLevel, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.Log(logLevel, message, args); } } /// /// Formats and writes a critical log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogCritical(this ILogger logger, EventId eventId, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogCritical(eventId, exception, message, args); } } /// /// Formats and writes a critical log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogCritical(this ILogger logger, EventId eventId, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogCritical(eventId, message, args); } } /// /// Formats and writes a critical log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogCritical(this ILogger logger, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogCritical(exception, message, args); } } /// /// Formats and writes a critical log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogCritical(this ILogger logger, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogCritical(message, args); } } /// /// Formats and writes a debug log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogDebug(this ILogger logger, EventId eventId, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogDebug(eventId, exception, message, args); } } /// /// Formats and writes a debug log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogDebug(this ILogger logger, EventId eventId, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogDebug(eventId, message, args); } } /// /// Formats and writes a debug log message /// Enhanced for PresenceLight with extended Context Logging /// /// The Microsoft.Extensions.Logging.ILogger to write to. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogDebug(this ILogger logger, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogDebug(exception, message, args); } } /// /// Formats and writes an error log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogDebug(this ILogger logger, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogDebug(message, args); } } /// /// Formats and writes an error log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogError(this ILogger logger, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogError(message, args); } } // // Summary: // Formats and writes an error log message. // Enhanced for PresenceLight with extended Context Logging // // Parameters: // logger: // The Microsoft.Extensions.Logging.ILogger to write to. // // exception: // The exception to log. // // message: // Format string of the log message in message template format. Example: "User {User} // logged in from {Address}" // // args: // An object array that contains zero or more objects to format. /// /// Formats and writes an error log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogError(this ILogger logger, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogError(exception, message, args); } } /// /// Formats and writes an error log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogError(this ILogger logger, EventId eventId, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogError(eventId, message, args); } } /// /// Formats and writes an error log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogError(this ILogger logger, EventId eventId, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogError(eventId, exception, message, args); } } /// /// Formats and writes an information log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogInformation(this ILogger logger, EventId eventId, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogInformation(eventId, exception, message, args); } } /// /// Formats and writes an information log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogInformation(this ILogger logger, EventId eventId, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogInformation(eventId, message, args); } } /// /// Formats and writes an information log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogInformation(this ILogger logger, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogInformation(exception, message, args); } } /// /// Formats and writes an information log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogInformation(this ILogger logger, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogInformation(message, args); } } /// /// Formats and writes a trace log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogTrace(this ILogger logger, EventId eventId, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogTrace(eventId, exception, message, args); } } /// /// Formats and writes a trace log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogTrace(this ILogger logger, EventId eventId, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogTrace(eventId, message, args); } } /// /// Formats and writes a trace log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogTrace(this ILogger logger, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogTrace(exception, message, args); } } /// /// Formats and writes a trace log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogTrace(this ILogger logger, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogTrace(message, args); } } /// /// Formats and writes a warning log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogWarning(this ILogger logger, EventId eventId, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogWarning(eventId, exception, message, args); } } /// /// Formats and writes a warning log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The event id associated with the log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogWarning(this ILogger logger, EventId eventId, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogWarning(eventId, message, args); } } /// /// Formats and writes a warning log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// The exception to log. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogWarning(this ILogger logger, Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogWarning(exception, message, args); } } /// /// Formats and writes a warning log message /// Enhanced for PresenceLight with extended Context Logging /// The Microsoft.Extensions.Logging.ILogger to write to. /// Format string of the log message. /// Membername occurring Note: Injected! /// File name where occurring Note: Injected! /// LineNumber where occurring Note: Injected! /// An object array that contains zero or more objects to format. public static void LogWarning(this ILogger logger, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, params object[] args) { using (Serilog.Context.LogContext.PushProperty("MemberName", memberName)) using (Serilog.Context.LogContext.PushProperty("FilePath", fileName)) using (Serilog.Context.LogContext.PushProperty("LineNumber", lineNumber)) { message = $"{message} - {fileName.Split("\\").LastOrDefault().Replace(".cs", "")}:{memberName} Line: {lineNumber}"; logger.LogWarning(message, args); } } } ================================================ FILE: src/PresenceLight.Core/Logging/PresenceEventsLogSink.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Serilog; using Serilog.Configuration; using Serilog.Core; using Serilog.Events; namespace PresenceLight.Core { /// /// Represents a log sink for presence events. /// public class PresenceEventsLogSink : ILogEventSink { private readonly IFormatProvider _formatProvider; /// /// Initializes a new instance of the class with the specified format provider. /// /// The format provider to be used for formatting log messages. public PresenceEventsLogSink(IFormatProvider formatProvider) { _formatProvider = formatProvider; } /// /// Emits a log event by invoking the PresenceEventsLogHandler delegate. /// /// The log event to emit. public void Emit(LogEvent logEvent) { PresenceEventsLogHandler?.Invoke(this, logEvent); } /// /// Represents the event handler for presence events logging. /// public static EventHandler PresenceEventsLogHandler; } /// /// Provides extension methods for adding the to the logger configuration. /// public static class PresenceEventsLogSinkExtensions { /// /// Adds the to the logger configuration. /// /// The logger configuration. /// The format provider. /// The logger configuration with the added . public static LoggerConfiguration PresenceEventsLogSink( this LoggerSinkConfiguration loggerConfiguration, IFormatProvider formatProvider = null) { return loggerConfiguration.Sink(new PresenceEventsLogSink(formatProvider)); } } } ================================================ FILE: src/PresenceLight.Core/PresenceLight.Core.csproj ================================================  net10.0 latest annotations true 1701;1702;1705;1591;NU1701;CS8618;CS8603;CS8600;CS8604;CS8602;CS8618 ================================================ FILE: src/PresenceLight.Razor/Components/Layout/MainLayout.razor ================================================ @inherits LayoutComponentBase @Body @code { bool _drawerOpen = true; void DrawerToggle() { _drawerOpen = !_drawerOpen; } } ================================================ FILE: src/PresenceLight.Razor/Components/Layout/MainLayout.razor.css ================================================ .page { position: relative; display: flex; flex-direction: column; } main { flex: 1; } .sidebar { background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); } .top-row { background-color: #f7f7f7; border-bottom: 1px solid #d6d5d5; justify-content: flex-end; height: 3.5rem; display: flex; align-items: center; } .top-row ::deep a, .top-row .btn-link { white-space: nowrap; margin-left: 1.5rem; } .top-row a:first-child { overflow: hidden; text-overflow: ellipsis; } @media (max-width: 640.98px) { .top-row:not(.auth) { display: none; } .top-row.auth { justify-content: space-between; } .top-row a, .top-row .btn-link { margin-left: 0; } } @media (min-width: 641px) { .page { flex-direction: row; } .sidebar { width: 250px; height: 100vh; position: sticky; top: 0; } .top-row { position: sticky; top: 0; z-index: 1; } .top-row, article { padding-left: 2rem !important; padding-right: 1.5rem !important; } } ================================================ FILE: src/PresenceLight.Razor/Components/Layout/NavMenu.razor ================================================  Teams Status Set Light Colors Philips Hue LIFX Yeelight Wiz Custom API Local Serial Host Settings Logs About @code { private bool collapseNavMenu = true; private bool expandSubMenu;//add private string expandClass = "oi oi-caret-bottom"; private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; private void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } private void ToggleExpand() { expandSubMenu = !expandSubMenu; if (expandSubMenu) { expandClass = "oi oi-caret-top"; } else { expandClass = "oi oi-caret-bottom"; } } } ================================================ FILE: src/PresenceLight.Razor/Components/Layout/NavMenu.razor.css ================================================ .navbar-toggler { background-color: rgba(255, 255, 255, 0.1); } .top-row { height: 3.5rem; background-color: rgba(0,0,0,0.4); } .navbar-brand { font-size: 1.1rem; } .oi { width: 2rem; font-size: 1.1rem; vertical-align: text-top; top: -2px; } .nav-item { font-size: 0.9rem; padding-bottom: 0.5rem; } .nav-item:first-of-type { padding-top: 1rem; } .nav-item:last-of-type { padding-bottom: 1rem; } .nav-item ::deep a { color: #d7d7d7; border-radius: 4px; height: 3rem; display: flex; align-items: center; line-height: 3rem; } .nav-item ::deep a.active { background-color: rgba(255,255,255,0.25); color: white; } .nav-item ::deep a:hover { background-color: rgba(255,255,255,0.1); color: white; } @media (min-width: 641px) { .navbar-toggler { display: none; } .collapse { /* Never collapse the sidebar for wide screens */ display: block; } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/About.razor ================================================ @page "/about" @inject IJSRuntime js @inject AppInfo _appInfo PresenceLight by Isaac Levin Application Type @appType Application Version @assemblyVersion Install Location @installLocation Install Date @installedDate Runtime Version @RuntimeVersionInfo Settings Path @localConfigurationPath

Found an issue or want a feature? File it here
@code { string installLocation; string installedDate; string RuntimeVersionInfo; string assemblyVersion; string localConfigurationPath; string appType; protected override void OnInitialized() { appType = _appInfo.GetAppInstallType(); installLocation = AppInfo.GetInstallLocation(); installedDate = AppInfo.GetInstallationDate(); RuntimeVersionInfo = AppInfo.GetDotNetRuntimeInfo(); assemblyVersion = _appInfo.GetApplicationVersion(); localConfigurationPath = new System.IO.FileInfo(SettingsService.GetSettingsFileLocation()).FullName; } async Task DownloadSettings(string filename) { string fileContents; using (var fs = new System.IO.FileStream( filename, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite)) { var b = new byte[fs.Length]; fs.Read(b, 0, b.Length); fileContents = Convert.ToBase64String(b); } await js.InvokeAsync( "saveAsFile", filename, fileContents); } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/Color.razor ================================================ @page "/color" @using LifxCloud.NET.Models @inject ILogger _logger; Set Light Color Set Color Sync Teams Presence @code { string color = "#FFFFFF"; async Task SetColor() { try { appState.SetLightMode("Custom"); _logger.LogInformation("Light Mode: Custom"); if (appState.LightMode == "Custom") { appState.SetCustomColor(color); _logger.LogInformation($"Custom Color: {color}"); } if (appState.LightMode == "Custom") { if (appState.Config.LightSettings.Hue.IsEnabled) { if (Helpers.AreStringsNotEmpty(new string[] {appState.Config.LightSettings.Hue.HueApiKey, appState.Config.LightSettings.Hue.SelectedItemId })) { if (appState.Config.LightSettings.Hue.UseRemoteApi) { if (!string.IsNullOrEmpty(appState.Config.LightSettings.Hue.RemoteBridgeId)) { await _mediator.Send(new Core.RemoteHueServices.SetColorCommand { Availability = appState.CustomColor, Activity = appState.CustomColor, LightId = appState.Config.LightSettings.Hue.SelectedItemId, BridgeId = appState.Config.LightSettings.Hue.RemoteBridgeId }); } } else { if (!string.IsNullOrEmpty(appState.Config.LightSettings.Hue.HueIpAddress)) { await _mediator.Send(new Core.HueServices.SetColorCommand() { Activity = appState.CustomColor, Availability = appState.CustomColor, LightID = appState.Config.LightSettings.Hue.SelectedItemId }); } } } } if (appState.Config.LightSettings.LIFX.IsEnabled && !string.IsNullOrEmpty(appState.Config.LightSettings.LIFX.LIFXApiKey)) { await _mediator.Send(new Core.LifxServices.SetColorCommand() { Availability = appState.CustomColor, Activity = appState.CustomColor, LightId = appState.Config.LightSettings.LIFX.SelectedItemId }); } if (appState.Config.LightSettings.Yeelight.IsEnabled && !string.IsNullOrEmpty(appState.Config.LightSettings.Yeelight.SelectedItemId)) { await _mediator.Send(new PresenceLight.Core.YeelightServices.SetColorCommand { Activity = appState.CustomColor, Availability = appState.CustomColor, LightId = appState.Config.LightSettings.Yeelight.SelectedItemId }); } if (appState.Config.LightSettings.CustomApi.IsEnabled) { string response = await _mediator.Send(new Core.CustomApiServices.SetColorCommand { Activity = appState.CustomColor, Availability = appState.CustomColor }); } if (appState.Config.LightSettings.LocalSerialHost.IsEnabled) { string response = await _mediator.Send(new Core.LocalSerialHostServices.SetColorCommand { Activity = appState.CustomColor, Availability = appState.CustomColor }); } if (appState.Config.LightSettings.Wiz.IsEnabled) { await _mediator.Send(new Core.WizServices.SetColorCommand { Activity = appState.CustomColor, Availability = appState.CustomColor, LightID = appState.Config.LightSettings.Wiz.SelectedItemId }); } } } catch (Exception e) { _logger.LogError(e, $"Error Occurred Setting Custom Color {color}"); throw; } } void SyncTeamsPresence() { appState.SetLightMode("Graph"); _logger.LogInformation("Light Mode: Graph"); } protected override void OnInitialized() { appState.OnChange += RaiseStateHasChanged; } public void Dispose() { appState.OnChange -= RaiseStateHasChanged; } private void RaiseStateHasChanged() { InvokeAsync(StateHasChanged); } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/Color.razor.css ================================================ ================================================ FILE: src/PresenceLight.Razor/Components/Pages/CustomApiSetup.razor ================================================ @page "/custom" @using PresenceLight.Core.Initialize; @inject ILogger _logger; Configure Custom API @if (appState.Config.LightSettings.CustomApi.IsEnabled) { @foreach (var customApiSetting in appState.Config.LightSettings.CustomApi.GetType().GetProperties()) { @if (customApiSetting.PropertyType.Name == "CustomApiSetting") { object customApiSettingValue = customApiSetting.GetValue(appState.Config.LightSettings.CustomApi, null); var lab = $"{customApiSetting.Name}Uri"; var body = $"{customApiSetting.Name}body"; @if (customApiSettingValue != null) { @foreach (var setting in customApiSettingValue.GetType().GetProperties()) { object settingValue = setting.GetValue(customApiSettingValue, null); if (setting.Name == "Method") { } else if (setting.Name == "Uri") { } else if (setting.Name == "Body") { } } } } } Use Basic Auth @if (appState.Config.LightSettings.CustomApi.UseBasicAuth) { Basic Auth Username: Basic Auth Password: } Ignore Certificate Errors } Save @if (settingsSaved) { @message } @code { bool settingsSaved = false; string message; protected override async Task OnInitializedAsync() { if (!appState.SignedIn) { NavManager.NavigateTo("/"); } try { appState.OnChange += RaiseStateHasChanged; } catch (Exception e) { _logger.LogError(e, "Error Occurred loading Custom API Page"); throw; } await Task.CompletedTask; } private void Save() { try { SettingsService.SaveSettings(appState.Config); // Check if non of the objects from appState.Config.LightSettings.CustomApi has a property method set without also having the property uri set. foreach (var customApiSetting in appState.Config.LightSettings.CustomApi.GetType().GetProperties()) { if (customApiSetting.PropertyType.Name == "CustomApiSetting") { object customApiSettingValue = customApiSetting.GetValue(appState.Config.LightSettings.CustomApi, null); foreach (var setting in customApiSettingValue.GetType().GetProperties()) { if (setting.Name == "Method") { object settingValue = setting.GetValue(customApiSettingValue, null); if (settingValue != null && settingValue.ToString() != "" && customApiSettingValue.GetType().GetProperty("Uri").GetValue(customApiSettingValue, null).ToString() == "") { _logger.LogError("Uri is required when Method is set"); throw new Exception("Uri is required when Method is set"); } } // Check if the Uri is set without the Method being set. if (setting.Name == "Uri") { object settingValue = setting.GetValue(customApiSettingValue, null); if (settingValue != null && settingValue.ToString() != "" && customApiSettingValue.GetType().GetProperty("Method").GetValue(customApiSettingValue, null).ToString() == "") { _logger.LogError("Method is required when Uri is set"); throw new Exception("Method is required when Uri is set"); } } } } } _mediator.Send(new InitializeCommand { AppState = appState }); message = "Settings Saved"; settingsSaved = true; _logger.LogInformation("Settings Saved from Custom API Page"); } catch (Exception e) { _logger.LogError(e, "Error Occurred Saving Custom API Settings"); throw; } } private void OnChange(ChangeEventArgs e, object setting, object customApiSettingValue) { var newSetting = e.Value; ((PropertyInfo)setting).SetValue(customApiSettingValue, newSetting); } public void Dispose() { appState.OnChange -= RaiseStateHasChanged; } private void RaiseStateHasChanged() { InvokeAsync(StateHasChanged); } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/HueSetup.razor ================================================ @page "/hue" @inject ILogger _logger; @inject IDialogService DialogService; Configure Phlips Hue Login to Hue Cloud @if (appState.Config.LightSettings.Hue.IsEnabled) { @if (!appState.Config.LightSettings.Hue.UseRemoteApi) { Find Hue Bridge


Register Bridge
} @if (showHueMessage) { @hueMessage } Get Hue Lights    Get Hue Groups if (isLoadingLights) {

} else { @if (appState.HueLights != null) { @foreach (var light in appState.HueLights) { if (light.GetType() == typeof(HueApi.Models.GroupedLight)) { var obj = (HueApi.Models.GroupedLight)light; @obj.Metadata?.Name } else { var obj = (HueApi.Models.Light)light; @obj.Metadata?.Name } } Brightness } } } Save @if (settingsSaved) { @message }
@code { bool settingsSaved = false; bool isLoadingLights = false; string message; string hueMessageClass; bool showHueMessage = false; string hueMessage; string selectedLightLabel = ""; string lastType = ""; protected override async Task OnInitializedAsync() { try { if (!appState.SignedIn) { NavManager.NavigateTo("/"); } await CheckHue(); appState.OnChange += RaiseStateHasChanged; } catch (Exception e) { _logger.LogError(e, "Error Occurred loading Hue Page"); throw; } } private async Task Save() { try { await SettingsService.SaveSettings(appState.Config); _mediator.Send(new Core.HueServices.InitializeCommand() { AppState = appState }).Wait(); message = "Settings Saved"; settingsSaved = true; _logger.LogInformation("Settings Saved from Hue Page"); } catch (Exception e) { _logger.LogError(e, "Error Occurred Saving Hue Settings"); throw; } } private async Task LoginRemoteApi() { if (appState.Config.LightSettings.Hue.UseRemoteApi) { if (!appState.Config.LightSettings.Hue.IsEnabled) { appState.Config.LightSettings.Hue.IsEnabled = true; } try { _logger.LogInformation("Cloud Hue Login Initialized"); var (bridgeId, apiKey, bridgeIp) = await _mediator.Send(new PresenceLight.Core.RemoteHueServices.RegisterBridgeCommand()); if (Helpers.AreStringsNotEmpty(new string[] { apiKey, bridgeId })) { appState.Config.LightSettings.Hue.HueApiKey = apiKey; appState.Config.LightSettings.Hue.RemoteBridgeId = bridgeId; appState.Config.LightSettings.Hue.HueIpAddress = bridgeIp; _logger.LogInformation("Cloud Hue Login Successful"); await Save(); } } catch (Exception ex) { _logger.LogError(ex, "Error Occurred Getting Cloud Hue Api Key"); } } } async Task FindBridge() { try { _logger.LogInformation("Hue Bridge Lookup Initialized"); appState.Config.LightSettings.Hue.HueIpAddress = await _mediator.Send(new PresenceLight.Core.HueServices.FindBridgeCommand()); _logger.LogInformation("Hue Bridge Lookup Successful"); } catch (Exception ex) { _logger.LogError(ex, "Error Occurred Getting Finding Hue Bridge"); } } async Task RegisterBridge() { _logger.LogInformation("Hue Bridge Registration Initialized"); var options = new DialogOptions { CloseOnEscapeKey = true }; var dialog = DialogService.Show("Please press the sync button on your Philips Hue Bridge", options); var result = await dialog.Result; if (result.Canceled) { _logger.LogInformation("Hue Bridge Registration Cancelled"); } else { try { appState.Config.LightSettings.Hue.HueApiKey = await _mediator.Send(new Core.HueServices.RegisterBridgeCommand()); _logger.LogInformation("Hue Bridge Registration Successful"); await Save(); } catch (Exception ex) { _logger.LogError(ex, "Error Occurred Registering Hue Bridge"); hueMessage = "Error Occurred registering bridge, please try again"; showHueMessage = true; hueMessageClass = "text-danger"; } if (!string.IsNullOrEmpty(appState.Config.LightSettings.Hue.HueApiKey)) { try { showHueMessage = true; hueMessage = "App Registered with Bridge"; hueMessageClass = "text-success"; appState.SetHueLights(await _mediator.Send(new PresenceLight.Core.HueServices.GetLightsCommand())); if (string.IsNullOrEmpty(appState.Config.LightSettings.Hue.SelectedItemId) && appState.HueLights.Count() > 0) { var firstLight = appState.HueLights.FirstOrDefault(); if (firstLight is HueApi.Models.Light light) { appState.Config.LightSettings.Hue.SelectedItemId = $"id:{light.IdV1}"; } } } catch (Exception e) { _logger.LogError(e, "Error Occurred Getting Hue Lights"); throw; } await Save(); } else { _logger.LogError("Hue Api Key not configured"); hueMessage = "Api Key Not Created, please try again and ensure you press the sync button on your bridge"; showHueMessage = true; hueMessageClass = "text-danger"; } } } async Task CheckHue(string type = null) { if (appState.Config.LightSettings.Hue.IsEnabled) { isLoadingLights = true; if (string.IsNullOrEmpty(lastType) || type != lastType) { if (string.IsNullOrEmpty(type)) { if (!string.IsNullOrEmpty(appState.Config.LightSettings.Hue.SelectedItemId)) { if (appState.Config.LightSettings.Hue.SelectedItemId.Contains("group")) { type = "Groups"; } else { type = "Lights"; } } else { appState.Config.LightSettings.Hue.SelectedItemId = string.Empty; type = "Lights"; } } else { appState.Config.LightSettings.Hue.SelectedItemId = string.Empty; selectedLightLabel = string.Empty; } lastType = type; } _logger.LogInformation($"Get Hue {type} Initialized"); if (!string.IsNullOrEmpty(appState.Config.LightSettings.Hue.HueApiKey)) { try { if (type == "Groups") { if (appState.Config.LightSettings.Hue.UseRemoteApi) { appState.SetHueLights(await _mediator.Send(new Core.RemoteHueServices.GetGroupsCommand())); } else { appState.SetHueLights(await _mediator.Send(new Core.HueServices.GetGroupsCommand())); } if (string.IsNullOrEmpty(appState.Config.LightSettings.Hue.SelectedItemId) && appState.HueLights.Count() > 0) { var firstGroup = appState.HueLights.FirstOrDefault(); if (firstGroup is HueApi.Models.GroupedLight group) { appState.Config.LightSettings.Hue.SelectedItemId = $"group_id:{group.IdV1}"; } } else { var matchingGroup = appState.HueLights.OfType() .Where(a => a.IdV1 == appState.Config.LightSettings.Hue.SelectedItemId.Replace("group_id:", "")) .FirstOrDefault(); if (matchingGroup != null) { selectedLightLabel = matchingGroup.Metadata?.Name; } } } else { if (appState.Config.LightSettings.Hue.UseRemoteApi) { appState.SetHueLights(await _mediator.Send(new Core.RemoteHueServices.GetLightsCommand())); } else { appState.SetHueLights(await _mediator.Send(new Core.HueServices.GetLightsCommand())); } if (string.IsNullOrEmpty(appState.Config.LightSettings.Hue.SelectedItemId) && appState.HueLights.Count() > 0) { var firstLight = appState.HueLights.FirstOrDefault(); if (firstLight is HueApi.Models.Light light) { appState.Config.LightSettings.Hue.SelectedItemId = $"id:{light.IdV1}"; } } else { var matchingLight = appState.HueLights.OfType() .Where(a => a.IdV1 == appState.Config.LightSettings.Hue.SelectedItemId.Replace("id:", "")) .FirstOrDefault(); if (matchingLight != null) { selectedLightLabel = matchingLight.Metadata?.Name; } } } await Save(); showHueMessage = true; hueMessage = "Connected to Hue"; hueMessageClass = "text-success"; _logger.LogInformation($"Get Hue {type} Successful"); } catch (Exception e) { _logger.LogError(e, "Error Occurred Getting Hue Lights"); showHueMessage = true; hueMessage = "Error Occurred Connecting to Hue, please try again"; hueMessageClass = "text-danger"; this.StateHasChanged(); appState.SetHueLights(new List()); } } isLoadingLights = false; } } private void OnChange(string e) { var light = e; appState.Config.LightSettings.Hue.SelectedItemId = light; appState.SetHueLight(light); if (lastType == "Groups") { var matchingGroup = appState.HueLights.OfType() .Where(a => a.IdV1 == appState.Config.LightSettings.Hue.SelectedItemId.Replace("group_id:", "")) .FirstOrDefault(); if (matchingGroup != null) { selectedLightLabel = matchingGroup.Metadata?.Name; } } else { var matchingLight = appState.HueLights.OfType() .Where(a => a.IdV1 == appState.Config.LightSettings.Hue.SelectedItemId.Replace("id:", "")) .FirstOrDefault(); if (matchingLight != null) { selectedLightLabel = matchingLight.Metadata?.Name; } } _logger.LogInformation($"Selected Hue Light Set: {light}"); } public void Dispose() { appState.OnChange -= RaiseStateHasChanged; } private void RaiseStateHasChanged() { InvokeAsync(StateHasChanged); } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/Index.razor ================================================ @page "/" @using Microsoft.Identity.Web @using Microsoft.Graph @inject ILogger _logger; @inject LoginService _loginService; @inject Microsoft.AspNetCore.Http.IHttpContextAccessor _httpContextAccessor @if (appState.SignedIn) {
@appState.User?.DisplayName
Availability: @Helpers.HumanifyText(appState.Presence?.Availability) Activity: @Helpers.HumanifyText(appState.Presence?.Activity)
} else if (appState.AadConfigComplete) { Login to Microsoft Account to Sync Presence Sign In } else { Enter Microsoft Entra / Azure AD configuration in Settings } @code { string image; protected override async Task OnInitializedAsync() { appState.OnChange += RaiseStateHasChanged; bool isUserAuth = await _loginService.IsUserAuthenticated(); if (!isUserAuth && appState.User == null) { appState.SetLightMode(""); if (appState.Config.AppType == "Web") { await _httpContextAccessor.HttpContext.ChallengeAsync(); } } else { if (appState.LightMode != "Custom") { appState.SignedIn = true; image = @appState.ProfileImage != null ? @appState.ProfileImage : "/_content/PresenceLight.Razor/images/unknownprofile.png"; appState.SetLightMode("Graph"); _logger.LogInformation("Light Mode: Graph"); } } } void SignIn() { _logger.LogInformation("Sign In Requested"); appState.SignInRequested = true; } public void Dispose() { appState.OnChange -= RaiseStateHasChanged; } private void RaiseStateHasChanged() { InvokeAsync(StateHasChanged); } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/Index.razor.css ================================================ .Available { background-color: green; } .AvailableIdle { background-color: yellow; } .Away { background-color: yellow; } .Busy { background-color: red; } .BeRightBack { background-color: yellow; } .DoNotDisturb { background-color: darkred; } .Offline { background-color: white; } .PresenceUnknown { background-color: white; } .bottom-right { position: relative; bottom: 120px; left: 55%; } .circle { height: 120px; width: 120px; border-radius: 50%; border-style: solid; border-width: 5px; border-color: white; } .image { width: 300px; height: 300px; border-radius: 50%; } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/Lifx.razor ================================================ @page "/lifx" @inject ILogger _logger; @inject LIFXOAuthHelper _lIFXOAuthHelper; Configure LIFX @if (appState.Config.LightSettings.LIFX.IsEnabled) {
LIFX Token
Get Token
Find LIFX Lights Find LIFX Groups if (isLoadingLights) {

} else { @if (appState.LIFXLights != null) { @foreach (var light in appState.LIFXLights) { if (light.GetType() == typeof(LifxCloud.NET.Models.Group)) { var obj = (LifxCloud.NET.Models.Group)light; @obj.Label } else { var obj = (LifxCloud.NET.Models.Light)light; @obj.Label } } Brightness } } } Save @if (settingsSaved) { @message }
@code { bool settingsSaved = false; string message; bool showLifxMessage = false; string lifxMessage; string lastType = ""; bool isLoadingLights = false; string selectedLightLabel = ""; protected override async Task OnInitializedAsync() { try { appState.OnChange += RaiseStateHasChanged; if (!appState.SignedIn) { NavManager.NavigateTo("/"); } await CheckLIFX(); } catch (Exception e) { _logger.LogError(e, "Error Occurred loading LIFX Setup"); throw; } } private async Task Save() { try { await SettingsService.SaveSettings(appState.Config); message = "Settings Saved"; settingsSaved = true; _logger.LogInformation("Settings Saved from LIFX Page"); } catch (Exception e) { _logger.LogError(e, "Error Occurred Saving Lifx Settings"); throw; } } async Task CheckLIFX(string type = null) { if (@appState.Config.LightSettings.LIFX.IsEnabled) { isLoadingLights = true; if (string.IsNullOrEmpty(lastType) || type != lastType) { if (string.IsNullOrEmpty(type)) { if (!string.IsNullOrEmpty(appState.Config.LightSettings.LIFX.SelectedItemId)) { if (appState.Config.LightSettings.LIFX.SelectedItemId.Contains("group")) { type = "Groups"; } else { type = "Lights"; } } else { appState.Config.LightSettings.LIFX.SelectedItemId = string.Empty; selectedLightLabel = string.Empty; type = "Lights"; } } else { appState.Config.LightSettings.LIFX.SelectedItemId = string.Empty; selectedLightLabel = string.Empty; } lastType = type; } _logger.LogInformation($"Get LIFX {type} Initialized"); if (!string.IsNullOrEmpty(appState.Config.LightSettings.LIFX.LIFXApiKey)) { try { if (type == "Groups") { appState.SetLIFXLights(await _mediator.Send(new Core.LifxServices.GetAllGroupsCommand() { ApiKey = appState.Config.LightSettings.LIFX.LIFXApiKey })); if (string.IsNullOrEmpty(appState.Config.LightSettings.LIFX.SelectedItemId) && appState.LIFXLights.Count() > 0) { var obj = (LifxCloud.NET.Models.Group)appState.LIFXLights.FirstOrDefault(); appState.Config.LightSettings.LIFX.SelectedItemId = $"group_id:{obj.Id}"; } else { selectedLightLabel = ((LifxCloud.NET.Models.Group)appState.LIFXLights.Where(a => ((LifxCloud.NET.Models.Group)a).Id == appState.Config.LightSettings.LIFX.SelectedItemId.Replace("group_id:", "")).FirstOrDefault()).Label; } } else { appState.SetLIFXLights(await _mediator.Send(new Core.LifxServices.GetAllLightsCommand() { ApiKey = appState.Config.LightSettings.LIFX.LIFXApiKey })); if (string.IsNullOrEmpty(appState.Config.LightSettings.LIFX.SelectedItemId) && appState.LIFXLights.Count() > 0) { var obj = (LifxCloud.NET.Models.Light)appState.LIFXLights.FirstOrDefault(); appState.Config.LightSettings.LIFX.SelectedItemId = $"id:{obj.Id}"; } else { selectedLightLabel = ((LifxCloud.NET.Models.Light)appState.LIFXLights.Where(a => ((LifxCloud.NET.Models.Light)a).Id == appState.Config.LightSettings.LIFX.SelectedItemId.Replace("id:", "")).FirstOrDefault()).Label; } } await Save(); showLifxMessage = true; lifxMessage = "Connected to LIFX Cloud"; _logger.LogInformation($"Get LIFX {type} Successful"); } catch (Exception e) { _logger.LogError(e, "Error Occurred Getting LIFX Lights"); showLifxMessage = true; lifxMessage = "Error Occurred Connecting to LIFX, please try again"; this.StateHasChanged(); appState.SetLIFXLights(new List()); } } isLoadingLights = false; } } async Task GetToken() { _logger.LogInformation("LIFX Token Retrieval Initialized"); if (!@appState.Config.LightSettings.LIFX.IsEnabled) { appState.Config.LightSettings.LIFX.IsEnabled = true; } var token = await _lIFXOAuthHelper.InitiateTokenRetrieval(); appState.Config.LightSettings.LIFX.LIFXApiKey = token; if (!@appState.Config.LightSettings.LIFX.IsEnabled) { appState.Config.LightSettings.LIFX.IsEnabled = true; } await Save(); _logger.LogInformation("LIFX Token Retrieveal Successful"); } private void OnChange(string e) { var light = e; appState.Config.LightSettings.LIFX.SelectedItemId = light; appState.SetLIFXLight(light); _logger.LogInformation($"Selected LIFX Light Set: {light}"); if (e.Contains("group")) { selectedLightLabel = ((LifxCloud.NET.Models.Group)appState.LIFXLights.Where(a => ((LifxCloud.NET.Models.Group)a).Id == appState.Config.LightSettings.LIFX.SelectedItemId.Replace("group_id:", "")).FirstOrDefault()).Label; } else { selectedLightLabel = ((LifxCloud.NET.Models.Light)appState.LIFXLights.Where(a => ((LifxCloud.NET.Models.Light)a).Id == appState.Config.LightSettings.LIFX.SelectedItemId.Replace("id:", "")).FirstOrDefault()).Label; } } public void Dispose() { appState.OnChange -= RaiseStateHasChanged; } private void RaiseStateHasChanged() { InvokeAsync(StateHasChanged); } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/LocalSerialHostSetup.razor ================================================ @page "/serial" @using PresenceLight.Core.LocalSerialHostServices; @inject ILogger _logger; Configure Local Serial @if (appState.Config.LightSettings.LocalSerialHost.IsEnabled) { Get Serial Ports if (isLoadingLights) {

} else { @if (appState.LocalSerialHosts != null) { @foreach (var LocalSerialHostSetting in appState.Config.LightSettings.LocalSerialHost.GetType().GetProperties()) { @if (LocalSerialHostSetting.PropertyType.Name == "LocalSerialHostSetting") { object LocalSerialHostSettingValue = LocalSerialHostSetting.GetValue(appState.Config.LightSettings.LocalSerialHost, null); var lab = $"{LocalSerialHostSetting.Name}Uri"; @if (LocalSerialHostSettingValue != null) { @foreach (var setting in LocalSerialHostSettingValue.GetType().GetProperties()) { object settingValue = setting.GetValue(LocalSerialHostSettingValue, null); if (setting.Name == "Port") { } else if (setting.Name == "BaudRate") { } else if (setting.Name == "LineEnding") { } } } } else if (LocalSerialHostSetting.PropertyType.Name == "String") { @if (LocalSerialHostSetting.Name != "SelectedItemId") { object parent = appState.Config.LightSettings.LocalSerialHost; object LocalSerialHostSettingValue = LocalSerialHostSetting.GetValue(appState.Config.LightSettings.LocalSerialHost, null); var lab = $"{LocalSerialHostSetting.Name}Uri"; } } } } } } Save @if (settingsSaved) { @message }
@code { bool settingsSaved = false; string message; bool isLoadingLights = false; string selectedPort = ""; protected override async Task OnInitializedAsync() { if (!appState.SignedIn) { NavManager.NavigateTo("/"); } try { appState.OnChange += RaiseStateHasChanged; await GetSerialHosts(); } catch (Exception e) { _logger.LogError(e, "Error Occurred loading Local Serial Host Page"); throw; } await Task.CompletedTask; } private void Save() { try { SettingsService.SaveSettings(appState.Config); _mediator.Send(new InitializeCommand { AppState = appState }); message = "Settings Saved"; settingsSaved = true; _logger.LogInformation("Settings Saved from Local Serial Host Page"); } catch (Exception e) { _logger.LogError(e, "Error Occurred Saving Local Serial Host Settings"); throw; } } private void OnChange(ChangeEventArgs e, object setting, object LocalSerialHostSettingValue, bool directChangeValue = false) { var newSetting = e.Value; if (directChangeValue) { string settingAsString = (string)setting; settingAsString = (string)newSetting; } else { PropertyInfo propInfo = (PropertyInfo)setting; try { propInfo.SetValue(LocalSerialHostSettingValue, newSetting); } catch (TargetException TargetEx) { _logger.LogError(TargetEx, "New value is wrong type"); } } } private void OnChange(string e) { var port = e; appState.Config.LightSettings.LocalSerialHost.SelectedItemId = port; appState.SetLocalSerialHost(port); _logger.LogInformation($"Selected Serial Host Set: {port}"); } public async Task GetSerialHosts() { if (appState.Config.LightSettings.LocalSerialHost.IsEnabled) { try { isLoadingLights = true; _logger.LogInformation("Local Serial Hosts Retrieval Initialized"); appState.SetLocalSerialHosts(await _mediator.Send(new Core.LocalSerialHostServices.GetPortCommand())); if (string.IsNullOrEmpty(appState.Config.LightSettings.LocalSerialHost.SelectedItemId) && appState.LocalSerialHosts.Count() > 0) { appState.Config.LightSettings.LocalSerialHost.SelectedItemId = appState.LocalSerialHosts.FirstOrDefault(); } else { selectedPort = appState.LocalSerialHosts.Where(a => a == appState.Config.LightSettings.LocalSerialHost.SelectedItemId).FirstOrDefault(); } _logger.LogInformation("Local Serial Hosts Retrieval Successful"); isLoadingLights = false; } catch (Exception e) { _logger.LogError(e, "Error occurred finding Serial Hosts"); throw; } } } public void Dispose() { appState.OnChange -= RaiseStateHasChanged; } private void RaiseStateHasChanged() { InvokeAsync(StateHasChanged); } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/Logs.razor ================================================ @page "/logs" @using System.IO @inject ILogger _logger @inject IJSRuntime js @inject Microsoft.Extensions.Configuration.IConfiguration _configuration @lock (logFilesLockObject) { foreach (var logFile in LogFiles.OrderByDescending(a => a.CreationTime)) { } }
FileName Size Creation Time Last Access
Open @logFile.Name @logFile.Length @logFile.CreationTime @logFile.LastAccessTime
@lock (logsLockObject) { foreach (var eEvent in InformationLogs.OrderByDescending(a => a.Timestamp)) { string style = null; switch (eEvent.Level) { case Serilog.Events.LogEventLevel.Warning: style = "background-color:yellow; color:red"; break; case Serilog.Events.LogEventLevel.Error: style = "background-color:red; color:yellow"; break; case Serilog.Events.LogEventLevel.Fatal: style = "background-color:red; color:white"; break; default: break; } } }
Timestamp Level Message
@eEvent.Timestamp @eEvent.Level @eEvent.RenderMessage()
@code { string selectedTab = "logfiles"; static object logsLockObject = new(); static object logFilesLockObject = new(); public string LogFilePath { get; set; } private Queue InformationLogs = new(25); List LogFiles = new(); private System.IO.FileSystemWatcher _watcher; protected override Task OnInitializedAsync() { PresenceEventsLogSink.PresenceEventsLogHandler += Handler; InitializeFileWatcher(); return base.OnInitializedAsync(); } private void OnSelectedTabChanged(string name) { selectedTab = name; } private void InitializeFileWatcher() { //TODO: May consider making this a bit mode robust in the future.. Assumes this config // IS always the second item in the config file. LogFilePath = _configuration["Serilog:WriteTo:1:Args:Path"]; if (string.IsNullOrWhiteSpace(LogFilePath)) return; LogFilePath = Environment.ExpandEnvironmentVariables(LogFilePath); if (LogFilePath.Contains('/')) LogFilePath = LogFilePath.Replace('/', '\\'); var fi = new FileInfo(LogFilePath); if (!string.IsNullOrWhiteSpace(fi.Extension)) { LogFilePath = fi.DirectoryName; } var di = new System.IO.DirectoryInfo(LogFilePath); if (di.Exists) { di.GetFiles().ToList().ForEach(d => LogFiles.Add(d)); } else { di.Create(); } _watcher = new System.IO.FileSystemWatcher(LogFilePath); _watcher.Deleted += Watcher_Changed; _watcher.Created += Watcher_Changed; _watcher.Changed += Watcher_Changed; _watcher.EnableRaisingEvents = true; } private void Watcher_Changed(object sender, System.IO.FileSystemEventArgs e) { switch (e.ChangeType) { case System.IO.WatcherChangeTypes.Created: lock (logFilesLockObject) { LogFiles.Add(new System.IO.FileInfo(e.FullPath)); } InvokeAsync(() => StateHasChanged()); break; case System.IO.WatcherChangeTypes.Changed: lock (logFilesLockObject) { LogFiles.RemoveAll(A => A.Name.Equals(e.Name, StringComparison.CurrentCultureIgnoreCase)); LogFiles.Add(new System.IO.FileInfo(e.FullPath)); } InvokeAsync(() => StateHasChanged()); break; case System.IO.WatcherChangeTypes.Deleted: lock (logFilesLockObject) { LogFiles.RemoveAll(A => A.Name.Equals(e.Name, StringComparison.CurrentCultureIgnoreCase)); } InvokeAsync(() => StateHasChanged()); break; } } private void Handler(object sender, Serilog.Events.LogEvent e) { lock (logsLockObject) { InformationLogs.Enqueue(e); } InvokeAsync(() => StateHasChanged()); } async Task DownloadLogs(string filename) { string fileContents; using (var fs = new System.IO.FileStream( System.IO.Path.Combine(LogFilePath, filename), System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite)) { var b = new byte[fs.Length]; fs.Read(b, 0, b.Length); fileContents = Convert.ToBase64String(b); } await js.InvokeAsync( "saveAsFile", filename, fileContents); } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/Settings.razor ================================================ @page "/settings" @inject IJSRuntime js @inject ILogger _logger; Settings Download File Microsoft Entra / Azure AD
@if (appState.Config.LightSettings.UseWorkingHours) { Working Days

Sun Mon Tues Wed Thu Fri Sat

Light Syncing Schedule (Working Hours)


Light Status When After Hours Are Reached Off White Keep } @if (appState.Config.LightSettings.UseDefaultBrightness) { Brightness } Polling Interval @if (appState.Config.AppType == "Desktop") { Icon Type Transparent White } Save Settings @if (settingsSaved) { @message }
@code { bool settingsSaved = false; string message; string localConfigurationPath; bool Monday; bool Tuesday; bool Wednesday; bool Thursday; bool Friday; bool Saturday; bool Sunday; TimeSpan? startTimeSpan; TimeSpan? endTimeSpan; protected override async Task OnInitializedAsync() { appState.Config.LightSettings.WorkingHoursStartTimeAsDate = string.IsNullOrEmpty(appState.Config.LightSettings.WorkingHoursStartTime) ? null : DateTime.Parse(appState.Config.LightSettings.WorkingHoursStartTime, null); appState.Config.LightSettings.WorkingHoursEndTimeAsDate = string.IsNullOrEmpty(appState.Config.LightSettings.WorkingHoursEndTime) ? null : DateTime.Parse(appState.Config.LightSettings.WorkingHoursEndTime, null); PopulateWorkingDays(); startTimeSpan = appState.Config.LightSettings.WorkingHoursStartTimeAsDate == null ? null : appState.Config.LightSettings.WorkingHoursStartTimeAsDate.Value.TimeOfDay; endTimeSpan = appState.Config.LightSettings.WorkingHoursEndTimeAsDate == null ? null : appState.Config.LightSettings.WorkingHoursEndTimeAsDate.Value.TimeOfDay; localConfigurationPath = new System.IO.FileInfo(SettingsService.GetSettingsFileLocation()).FullName; await Task.CompletedTask; } void SaveSettings() { try { SetWorkingDays(); appState.Config.LightSettings.WorkingHoursStartTime = startTimeSpan.ToString(); appState.Config.LightSettings.WorkingHoursEndTime = endTimeSpan.ToString(); SettingsService.SaveSettings(appState.Config); message = "Settings Saved"; settingsSaved = true; _logger.LogInformation("Settings Saved from Settings Page"); } catch (Exception e) { _logger.LogError(e, "Error Occurred Saving Settings from Settings Page"); throw; } } private void PopulateWorkingDays() { if (!string.IsNullOrEmpty(appState.Config.LightSettings.WorkingDays)) { if (appState.Config.LightSettings.WorkingDays.Contains("Monday", StringComparison.OrdinalIgnoreCase)) { Monday = true; } if (appState.Config.LightSettings.WorkingDays.Contains("Tuesday", StringComparison.OrdinalIgnoreCase)) { Tuesday = true; } if (appState.Config.LightSettings.WorkingDays.Contains("Wednesday", StringComparison.OrdinalIgnoreCase)) { Wednesday = true; } if (appState.Config.LightSettings.WorkingDays.Contains("Thursday", StringComparison.OrdinalIgnoreCase)) { Thursday = true; } if (appState.Config.LightSettings.WorkingDays.Contains("Friday", StringComparison.OrdinalIgnoreCase)) { Friday = true; } if (appState.Config.LightSettings.WorkingDays.Contains("Saturday", StringComparison.OrdinalIgnoreCase)) { Saturday = true; } if (appState.Config.LightSettings.WorkingDays.Contains("Sunday", StringComparison.OrdinalIgnoreCase)) { Sunday = true; } } } private void SetWorkingDays() { List days = new List(); if (Monday) { days.Add("Monday"); } if (Tuesday) { days.Add("Tuesday"); } if (Wednesday) { days.Add("Wednesday"); } if (Thursday) { days.Add("Thursday"); } if (Friday) { days.Add("Friday"); } if (Saturday) { days.Add("Saturday"); } if (Sunday) { days.Add("Sunday"); } appState.Config.LightSettings.WorkingDays = string.Join("|", days); } async Task DownloadSettings(string filename) { string fileContents; using (var fs = new System.IO.FileStream( filename, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite)) { var b = new byte[fs.Length]; fs.Read(b, 0, b.Length); fileContents = Convert.ToBase64String(b); } await js.InvokeAsync( "saveAsFile", filename, fileContents); } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/Wiz.razor ================================================ @page "/wiz" @inject ILogger _logger; Configure Wiz @if (appState.Config.LightSettings.Wiz.IsEnabled) {
Get Light
@if (showWizMessage) { @wizMessage } if (isLoadingLights) {

} else { @if (appState.WizLight != null) { Brightness } } } Save @if (settingsSaved) { @message }
@code { bool settingsSaved = false; bool isLoadingLights = false; string message; string wizMessageClass; bool showWizMessage = false; string wizMessage; string selectedLightLabel = ""; string ipAddress = ""; protected override async Task OnInitializedAsync() { try { appState.OnChange += RaiseStateHasChanged; if (!appState.SignedIn) { NavManager.NavigateTo("/"); } await GetLight(); } catch (Exception e) { _logger.LogError(e, "Error Occurred loading Wiz"); throw; } await Task.CompletedTask; } private async Task Save() { try { await SettingsService.SaveSettings(appState.Config); message = "Settings Saved"; settingsSaved = true; _logger.LogInformation("Settings Saved from Wiz Page"); } catch (Exception e) { _logger.LogError(e, "Error Occurred Saving Wiz Settings"); throw; } } public async Task GetLight() { if (appState.Config.LightSettings.Wiz.IsEnabled && !string.IsNullOrEmpty(appState.Config.LightSettings.Wiz.IPAddress)) { if (System.Text.RegularExpressions.Regex.IsMatch( appState.Config.LightSettings.Wiz.IPAddress, @"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")) { try { isLoadingLights = true; _logger.LogInformation("Wiz Light Retrieval Initialized"); appState.SetWizLight(await _mediator.Send(new Core.WizServices.GetLightCommand())); if (string.IsNullOrEmpty(appState.Config.LightSettings.Wiz.SelectedItemId) && appState.WizLight != null) { appState.Config.LightSettings.Wiz.SelectedItemId = appState.WizLight.MacAddress; } else { selectedLightLabel = appState.WizLight.LightName; } _logger.LogInformation("Wiz Light Retrieval Successful"); showWizMessage = true; wizMessage = $"Connected to {appState.WizLight.LightName}"; wizMessageClass = "text-success"; isLoadingLights = false; } catch (Exception ex) { _logger.LogError(ex, "Error occurred Finding Wiz Lights"); wizMessage = "Error Occurred finding light, please try another IP Address"; showWizMessage = true; wizMessageClass = "text-danger"; throw; } } else { wizMessage = "Not a valid IP Address"; showWizMessage = true; wizMessageClass = "text-danger"; } } } public void Dispose() { appState.OnChange -= RaiseStateHasChanged; } private void RaiseStateHasChanged() { InvokeAsync(StateHasChanged); } } ================================================ FILE: src/PresenceLight.Razor/Components/Pages/Yeelight.razor ================================================ @page "/yeelight" @inject ILogger _logger; Configure Yeelight @if (appState.Config.LightSettings.Yeelight.IsEnabled) { Find Yeelights if (isLoadingLights) {

} else { @if (appState.YeelightLights != null) { @foreach (var light in appState.YeelightLights) { @light.Hostname } Brightness } } } Save @if (settingsSaved) { @message }
@code { bool settingsSaved = false; string message; bool isLoadingLights = false; string selectedLightLabel = ""; protected override async Task OnInitializedAsync() { try { appState.OnChange += RaiseStateHasChanged; if (!appState.SignedIn) { NavManager.NavigateTo("/"); } await GetYeeLights(); } catch (Exception e) { _logger.LogError(e, "Error Occurred loading Yeelight"); throw; } await Task.CompletedTask; } private async Task Save() { try { await SettingsService.SaveSettings(appState.Config); message = "Settings Saved"; settingsSaved = true; _logger.LogInformation("Settings Saved from Yeelight Page"); } catch (Exception e) { _logger.LogError(e, "Error Occurred Saving Yeelight Settings"); throw; } } private void OnChange(string e) { var light = e; appState.Config.LightSettings.Yeelight.SelectedItemId = light; appState.SetYeelightLight(light); _logger.LogInformation($"Selected Yeelight Light Set: {light}"); } public async Task GetYeeLights() { if (@appState.Config.LightSettings.Yeelight.IsEnabled) { try { isLoadingLights = true; _logger.LogInformation("Yeelight Light Retrieval Initialized"); appState.SetYeelightLights(await _mediator.Send(new Core.YeelightServices.GetLightCommand())); if (string.IsNullOrEmpty(appState.Config.LightSettings.Yeelight.SelectedItemId) && appState.YeelightLights.Count() > 0) { appState.Config.LightSettings.Yeelight.SelectedItemId = appState.YeelightLights.FirstOrDefault().Id; } else { selectedLightLabel = appState.YeelightLights.Where(a => a.Id == appState.Config.LightSettings.Yeelight.SelectedItemId).FirstOrDefault()?.Hostname; } _logger.LogInformation("Yeelight Light Retrieval Successful"); isLoadingLights = false; } catch (Exception ex) { _logger.LogError(ex, "Error occurred Finding YeeLights"); throw; } } } public void Dispose() { appState.OnChange -= RaiseStateHasChanged; } private void RaiseStateHasChanged() { InvokeAsync(StateHasChanged); } } ================================================ FILE: src/PresenceLight.Razor/Components/PresenceLightClientApp.razor ================================================  ================================================ FILE: src/PresenceLight.Razor/Components/Shared/Confirm.razor ================================================  Ok @code { [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } void Submit() => MudDialog.Close(DialogResult.Ok(true)); } ================================================ FILE: src/PresenceLight.Razor/Components/Shared/LoginDisplay.razor ================================================ @inject AppState appState @inject NavigationManager NavManager @if (appState.Config.AppType == "Desktop") { @if (appState.SignedIn && appState.User != null) { Hello, @appState.User.DisplayName! } else { } } else { Hello, @context.User.Identity.Name!
Log Out
Log in
} @code { protected override void OnInitialized() { appState.OnChange += RaiseStateHasChanged; } private void SignIn() { appState.SignInRequested = true; } private void SignOut() { if (appState.Config.AppType == "Desktop") { appState.SignOutRequested = true; NavManager.NavigateTo("/"); } else { NavManager.NavigateTo("MicrosoftIdentity/Account/SignOut"); } } public void Dispose() { appState.OnChange -= RaiseStateHasChanged; } private void RaiseStateHasChanged() { InvokeAsync(StateHasChanged); } } ================================================ FILE: src/PresenceLight.Razor/Components/Shared/Statuses.razor ================================================ 

Custom Colors

@foreach (var lightStatus in Light.Statuses.GetType().GetProperties().OrderBy(a => a.Name)) { @if ((!Light.UseActivityStatus && lightStatus.Name.Contains("Availability")) || (Light.UseActivityStatus && lightStatus.Name.Contains("Activity"))) { var status = (AvailabilityStatus)lightStatus.GetValue(Light.Statuses); @Helpers.HumanifyText(lightStatus.Name.Replace("Status", "").Replace("Availability", "").Replace("Activity", "")) Color } }
@code { [Parameter] public BaseLight Light { get; set; } void StatusDisabledOnCheck(bool e, object settingValue) { var newSetting = e; AvailabilityStatus status = (AvailabilityStatus)settingValue; status.Disabled = newSetting; } void ChangeStatusColor(ChangeEventArgs e, object settingValue) { var newSetting = e.Value; AvailabilityStatus status = (AvailabilityStatus)settingValue; status.Color = (string)newSetting; } } ================================================ FILE: src/PresenceLight.Razor/Components/_Imports.razor ================================================ @using System.Net.Http @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop @using PresenceLight.Razor @using PresenceLight.Razor.Components @using PresenceLight.Razor.Components.Shared @using PresenceLight.Razor.Components.Layout @using Microsoft.AspNetCore.Authentication @using PresenceLight.Core @using Microsoft.Extensions.Options @using Newtonsoft.Json @using Microsoft.Graph @using System @using System.Net @using System.Net.Http.Headers @using System.Threading.Tasks @using System.Threading @using BlazorPro.Spinkit @using Microsoft.Extensions.Logging @using PresenceLight.Razor.Services @using PresenceLight @using System.Reflection @using Blazorise @using MudBlazor @inject MediatR.IMediator _mediator @inject ISettingsService SettingsService @inject AppState appState @inject NavigationManager NavManager ================================================ FILE: src/PresenceLight.Razor/PresenceLight.Razor.csproj ================================================  net10.0 ================================================ FILE: src/PresenceLight.Razor/Services/AppInfo.cs ================================================ using System; using System.Diagnostics; using System.Globalization; using System.Reflection; using Microsoft.Extensions.Configuration; namespace PresenceLight.Razor { public class AppInfo { private readonly IConfiguration _config; public AppInfo(IConfiguration Configuration) { _config = Configuration; } public static string GetInstallLocation() { return System.AppContext.BaseDirectory; } public static string GetInstallationDate() { var date = System.IO.File.GetLastWriteTime(System.AppContext.BaseDirectory); return $"{date.ToShortDateString()} {date.ToShortTimeString()}"; } public string GetApplicationVersion() { return _config["AppVersion"]; } public static string GetDotNetRuntimeInfo() { return typeof(object).Assembly.GetCustomAttribute().InformationalVersion; } public string GetAppInstallType() { if (Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true") { return "Container"; } if (_config["AppType"] == "Web") { return "Web"; } if (new DesktopBridge.Helpers().IsRunningAsUwp()) { return "AppPackage"; } else { return "Standalone"; } } } } ================================================ FILE: src/PresenceLight.Razor/Services/AppVersionTelemetryInitializer.cs ================================================ using System.Diagnostics; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.Extensibility; namespace PresenceLight.Razor.Services { public class AppVersionTelemetryInitializer : ITelemetryInitializer { private readonly AppInfo _appInfo; public AppVersionTelemetryInitializer(AppInfo appInfo) { _appInfo = appInfo; } public void Initialize(ITelemetry telemetry) { telemetry.Context.Component.Version = _appInfo.GetApplicationVersion(); telemetry.Context.GlobalProperties["App Version"] = _appInfo.GetApplicationVersion(); telemetry.Context.GlobalProperties["App Install Type"] = _appInfo.GetAppInstallType(); } } } ================================================ FILE: src/PresenceLight.Razor/Services/WebAppSettingsService.cs ================================================ using System; using System.Diagnostics; using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using PresenceLight.Core; namespace PresenceLight.Razor.Services { public class WebAppSettingsService : ISettingsService { readonly IConfiguration _configuration; private readonly AppState _appState; public WebAppSettingsService(IConfiguration configuration, AppState appState) { _appState = appState; _configuration = configuration; } public Task DeleteSettings() { if (File.Exists(GetSettingsFileLocation())) { File.Delete(GetSettingsFileLocation()); } return Task.Run(() => true); } public async Task IsFilePresent() { if (!File.Exists(GetSettingsFileLocation())) { return false; } else { var config = await LoadSettings(); if (config == null) { return false; } } return true; } public async Task LoadSettings() { string fileJSON = await File.ReadAllTextAsync(GetSettingsFileLocation(), Encoding.UTF8); var config = JsonConvert.DeserializeObject(fileJSON); _appState.SetConfig(config); return config; } public async Task SaveSettings(BaseConfig data) { string content = JsonConvert.SerializeObject(data, Newtonsoft.Json.Formatting.Indented, new JsonSerializerSettings { }); await File.WriteAllTextAsync(GetSettingsFileLocation(), content, Encoding.UTF8); _appState.SetConfig(data); return true; } public string GetSettingsFileLocation() { if (_configuration?["DOTNET_RUNNING_IN_CONTAINER"] == "true") { return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "config", "PresenceLightSettings.json"); } else if (Debugger.IsAttached) { return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "PresenceLightSettings.Development.json"); } else { return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "PresenceLightSettings.json"); } } } } ================================================ FILE: src/PresenceLight.Razor/wwwroot/css/open-iconic/FONT-LICENSE ================================================ SIL OPEN FONT LICENSE Version 1.1 Copyright (c) 2014 Waybury PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: src/PresenceLight.Razor/wwwroot/css/open-iconic/ICON-LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 Waybury Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: src/PresenceLight.Razor/wwwroot/css/open-iconic/README.md ================================================ [Open Iconic v1.1.1](http://useiconic.com/open) =========== ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) ## What's in Open Iconic? * 223 icons designed to be legible down to 8 pixels * Super-light SVG files - 61.8 for the entire set * SVG sprite—the modern replacement for icon fonts * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. ## Getting Started #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. ### General Usage #### Using Open Iconic's SVGs We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). ``` icon name ``` #### Using Open Iconic's SVG Sprite Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* ``` ``` Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. ``` .icon { width: 16px; height: 16px; } ``` Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. ``` .icon-account-login { fill: #f00; } ``` To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). #### Using Open Iconic's Icon Font... ##### …with Bootstrap You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` ``` ``` ``` ``` ##### …with Foundation You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` ``` ``` ``` ``` ##### …on its own You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` ``` ``` ``` ``` ## License ### Icons All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). ### Fonts All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). ================================================ FILE: src/PresenceLight.Razor/wwwroot/css/site.css ================================================ @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } a, .btn-link { color: #0366d6; } .btn-primary { color: #fff; background-color: #1b6ec2; border-color: #1861ac; } .content { padding-top: 1.1rem; } .valid.modified:not([type=checkbox]) { outline: 1px solid #26b050; } .centering { float: none; margin: 0 auto; } .invalid { outline: 1px solid red; } .validation-message { color: red; } #blazor-error-ui { background: lightyellow; bottom: 0; box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); display: none; left: 0; padding: 0.6rem 1.25rem 0.7rem 1.25rem; position: fixed; width: 100%; z-index: 1000; } #blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 0.75rem; top: 0.5rem; } .blazor-error-boundary { background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; padding: 1rem 1rem 1rem 3.7rem; color: white; } .blazor-error-boundary::after { content: "An error has occurred." } .mud-picker-color-content { align-items: center; } ================================================ FILE: src/PresenceLight.Razor/wwwroot/js/site.js ================================================ function saveAsFile(filename, bytesBase64) { var link = document.createElement('a'); link.download = filename; link.href = "data:application/octet-stream;base64," + bytesBase64; document.body.appendChild(link); // Needed for Firefox link.click(); document.body.removeChild(link); } ================================================ FILE: src/PresenceLight.Web/.config/dotnet-tools.json ================================================ { "version": 1, "isRoot": true, "tools": { "microsoft.dotnet-msidentity": { "version": "1.0.0-preview.2.21302.1", "commands": [ "dotnet-msidentity" ] } } } ================================================ FILE: src/PresenceLight.Web/App.razor ================================================  PresenceLight
An unhandled error has occurred. Reload 🗙
================================================ FILE: src/PresenceLight.Web/AppOld.razor ================================================ @using Microsoft.AspNetCore.Components.Authorization @using PresenceLight.Razor.Components @using PresenceLight.Razor.Components.Layout @using Microsoft.AspNetCore.Components.Routing

Sorry, there's nothing at this address.

================================================ FILE: src/PresenceLight.Web/Dockerfile ================================================ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY ["PresenceLight.Web/PresenceLight.Web.csproj", "PresenceLight.Web/"] COPY ["PresenceLight.Razor/PresenceLight.Razor.csproj", "PresenceLight.Razor/"] COPY ["PresenceLight.Core/PresenceLight.Core.csproj", "PresenceLight.Core/"] RUN dotnet restore "PresenceLight.Web/PresenceLight.Web.csproj" COPY . . WORKDIR "/src/PresenceLight.Web" RUN dotnet build "PresenceLight.Web.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "PresenceLight.Web.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . LABEL org.opencontainers.image.source=https://github.com/isaacrlevin/presencelight ENTRYPOINT ["dotnet", "PresenceLight.dll"] ================================================ FILE: src/PresenceLight.Web/Dockerfile.debian-arm32 ================================================ FROM mcr.microsoft.com/dotnet/aspnet:9.0.0-bookworm-slim-arm32v7 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY ["PresenceLight.Web/PresenceLight.Web.csproj", "PresenceLight.Web/"] COPY ["PresenceLight.Razor/PresenceLight.Razor.csproj", "PresenceLight.Razor/"] COPY ["PresenceLight.Core/PresenceLight.Core.csproj", "PresenceLight.Core/"] RUN dotnet restore "PresenceLight.Web/PresenceLight.Web.csproj" COPY . . WORKDIR "/src/PresenceLight.Web" RUN dotnet build "PresenceLight.Web.csproj" -c Release -o /app/build -r linux-arm --self-contained false /p:Version={VERSION} FROM build AS publish RUN dotnet publish "PresenceLight.Web.csproj" -c Release -o /app/publish -r linux-arm --self-contained false /p:Version={VERSION} FROM base AS final WORKDIR /app COPY --from=publish /app/publish . LABEL org.opencontainers.image.source=https://github.com/isaacrlevin/presencelight ENTRYPOINT ["dotnet", "PresenceLight.dll"] ================================================ FILE: src/PresenceLight.Web/Dockerfile.debian-arm64 ================================================ FROM mcr.microsoft.com/dotnet/aspnet:9.0.0-bookworm-slim-arm64v8 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY ["PresenceLight.Web/PresenceLight.Web.csproj", "PresenceLight.Web/"] COPY ["PresenceLight.Razor/PresenceLight.Razor.csproj", "PresenceLight.Razor/"] COPY ["PresenceLight.Core/PresenceLight.Core.csproj", "PresenceLight.Core/"] RUN dotnet restore "PresenceLight.Web/PresenceLight.Web.csproj" COPY . . WORKDIR "/src/PresenceLight.Web" RUN dotnet build "PresenceLight.Web.csproj" -c Release -o /app/build -r linux-arm64 --self-contained false /p:Version={VERSION} FROM build AS publish RUN dotnet publish "PresenceLight.Web.csproj" -c Release -o /app/publish -r linux-arm64 --self-contained false /p:Version={VERSION} FROM base AS final WORKDIR /app COPY --from=publish /app/publish . LABEL org.opencontainers.image.source=https://github.com/isaacrlevin/presencelight ENTRYPOINT ["dotnet", "PresenceLight.dll"] ================================================ FILE: src/PresenceLight.Web/Pages/Error.cshtml ================================================ @page @model PresenceLight.Web.Pages.ErrorModel Error

Error.

An error occurred while processing your request.

@if (Model.ShowRequestId) {

Request ID: @Model.RequestId

}

Development Mode

Swapping to the Development environment displays detailed information about the error that occurred.

The Development environment shouldn't be enabled for deployed applications. It can result in displaying sensitive information from exceptions to end users. For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app.

================================================ FILE: src/PresenceLight.Web/Pages/Error.cshtml.cs ================================================ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; namespace PresenceLight.Web.Pages { [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [IgnoreAntiforgeryToken] public class ErrorModel : PageModel { public string RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); private readonly ILogger _logger; public ErrorModel(ILogger logger) { _logger = logger; } public void OnGet() { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; } } } ================================================ FILE: src/PresenceLight.Web/Pages/_Host.cshtml ================================================ @page "/" @using PresenceLight.Razor @using PresenceLight.Web @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @{ Layout = null; } PresenceLight
An error has occurred. This application may no longer respond until reloaded. An unhandled exception has occurred. See browser dev tools for details. Reload 🗙
An error has occurred. This application may no longer respond until reloaded. An unhandled exception has occurred. See browser dev tools for details. Reload 🗙
================================================ FILE: src/PresenceLight.Web/PresenceLight.Web.csproj ================================================  net10.0 latest PresenceLight enable wwwroot\favicon.ico 8509488430545 Linux Dockerfile .. Regular ..\DockerCompose\docker-compose.dcproj PreserveNewest true PreserveNewest Always Always ================================================ FILE: src/PresenceLight.Web/PresenceLightSettings.json ================================================ { "IconType": "", "LightSettings": { "HoursPassedStatus": "Keep", "SyncLights": true, "WorkingDays": "Monday|Tuesday|Wednesday|Thursday|Friday", "WorkingHoursStartTime": "", "WorkingHoursEndTime": "", "UseAmPm": true, "UseWorkingHours": false, "PollingInterval": 5.0, "UseDefaultBrightness": true, "DefaultBrightness": 100, "LIFX": { "LIFXClientId": "", "LIFXClientSecret": "", "LIFXApiKey": "", "IsEnabled": false, "SelectedItemId": "", "Brightness": 100, "UseActivityStatus": false, "Statuses": { "AvailabilityAvailableStatus": { "Disabled": false, "Color": "#00ff55" }, "AvailabilityAvailableIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityAwayStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBeRightBackStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBusyStatus": { "Disabled": false, "Color": "#FF3300" }, "AvailabilityBusyIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityDoNotDisturbStatus": { "Disabled": false, "Color": "#B03CDE" }, "AvailabilityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityOffStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityAvailableStatus": { "Disabled": false, "Color": "#4f824f" }, "ActivityAwayStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBeRightBackStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBusyStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityDoNotDisturbStatus": { "Disabled": false, "Color": "#960000" }, "ActivityInACallStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityInAConferenceCallStatus": { "Disabled": false, "Color": "#ff00d4" }, "ActivityInactiveStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityInAMeetingStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOffWorkStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOutOfOfficeStatus": { "Disabled": false, "Color": "#ae00ff" }, "ActivityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityPresentingStatus": { "Disabled": false, "Color": "#960000" }, "ActivityUrgentInterruptionsOnlyStatus": { "Disabled": false, "Color": "#560061" }, "ActivityOffStatus": { "Disabled": false, "Color": "#FFFFFF" } } }, "Hue": { "HueApiKey": "", "SelectedItemId": "", "HueIpAddress": "", "RemoteHueClientId": "", "RemoteHueClientSecret": "", "RemoteHueClientAppName": "", "IsEnabled": false, "RemoteBridgeId": "", "UseRemoteApi": false, "Brightness": 100, "UseActivityStatus": false, "Statuses": { "AvailabilityAvailableStatus": { "Disabled": false, "Color": "#00ff55" }, "AvailabilityAvailableIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityAwayStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBeRightBackStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBusyStatus": { "Disabled": false, "Color": "#FF3300" }, "AvailabilityBusyIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityDoNotDisturbStatus": { "Disabled": false, "Color": "#B03CDE" }, "AvailabilityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityOffStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityAvailableStatus": { "Disabled": false, "Color": "#4f824f" }, "ActivityAwayStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBeRightBackStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBusyStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityDoNotDisturbStatus": { "Disabled": false, "Color": "#960000" }, "ActivityInACallStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityInAConferenceCallStatus": { "Disabled": false, "Color": "#ff00d4" }, "ActivityInactiveStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityInAMeetingStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOffWorkStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOutOfOfficeStatus": { "Disabled": false, "Color": "#ae00ff" }, "ActivityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityPresentingStatus": { "Disabled": false, "Color": "#960000" }, "ActivityUrgentInterruptionsOnlyStatus": { "Disabled": false, "Color": "#560061" }, "ActivityOffStatus": { "Disabled": false, "Color": "#FFFFFF" } } }, "Yeelight": { "SelectedItemId": "", "IsEnabled": false, "Brightness": 100, "UseActivityStatus": false, "Statuses": { "AvailabilityAvailableStatus": { "Disabled": false, "Color": "#00ff55" }, "AvailabilityAvailableIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityAwayStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBeRightBackStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBusyStatus": { "Disabled": false, "Color": "#FF3300" }, "AvailabilityBusyIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityDoNotDisturbStatus": { "Disabled": false, "Color": "#B03CDE" }, "AvailabilityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityOffStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityAvailableStatus": { "Disabled": false, "Color": "#4f824f" }, "ActivityAwayStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBeRightBackStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBusyStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityDoNotDisturbStatus": { "Disabled": false, "Color": "#960000" }, "ActivityInACallStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityInAConferenceCallStatus": { "Disabled": false, "Color": "#ff00d4" }, "ActivityInactiveStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityInAMeetingStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOffWorkStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOutOfOfficeStatus": { "Disabled": false, "Color": "#ae00ff" }, "ActivityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityPresentingStatus": { "Disabled": false, "Color": "#960000" }, "ActivityUrgentInterruptionsOnlyStatus": { "Disabled": false, "Color": "#560061" }, "ActivityOffStatus": { "Disabled": false, "Color": "#FFFFFF" } } }, "CustomApi": { "IsEnabled": false, "SelectedItemId": "", "Brightness": 100, "UseActivityStatus": false, "CustomApiAvailable": { "Method": "", "Uri": "", "Body": "" }, "CustomApiBusy": { "Method": "", "Uri": "", "Body": "" }, "CustomApiBeRightBack": { "Method": "", "Uri": "", "Body": "" }, "CustomApiAway": { "Method": "", "Uri": "", "Body": "" }, "CustomApiDoNotDisturb": { "Method": "", "Uri": "", "Body": "" }, "CustomApiOffline": { "Method": "", "Uri": "", "Body": "" }, "CustomApiOff": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityAvailable": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityInACall": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityInAConferenceCall": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityInAMeeting": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityPresenting": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityBusy": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityAway": { "Method": "", "Uri": "", "Body": "" }, "CustomApiAvailableIdle": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityBeRightBack": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityDoNotDisturb": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityIdle": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityOffline": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityOff": { "Method": "", "Uri": "", "Body": "" }, "CustomApiActivityOffWork": { "Method": "", "Uri": "", "Body": "" }, "CustomApiTimeout": 100, "IgnoreCertificateErrors": false, "UseBasicAuth": false, "BasicAuthUserName": "", "BasicAuthUserPassword": "" }, "LocalSerialHost": { "IsEnabled": false, "SelectedItemId": "", "Brightness": 100, "UseActivityStatus": false, "BaudRate": "", "PortNumber": "", "LocalSerialHostMainSetup": { "BaudRate": "", "LineEnding": "", "Port": "" }, "LocalSerialHostAvailable": "", "LocalSerialHostBusy": "", "LocalSerialHostBeRightBack": "", "LocalSerialHostAway": "", "LocalSerialHostDoNotDisturb": "", "LocalSerialHostOffline": "", "LocalSerialHostOff": "", "LocalSerialHostActivityAvailable": "", "LocalSerialHostActivityInACall": "", "LocalSerialHostActivityInAConferenceCall": "", "LocalSerialHostActivityInAMeeting": "", "LocalSerialHostActivityPresenting": "", "LocalSerialHostActivityBusy": "", "LocalSerialHostActivityAway": "", "LocalSerialHostAvailableIdle": "", "LocalSerialHostActivityBeRightBack": "", "LocalSerialHostActivityDoNotDisturb": "", "LocalSerialHostActivityIdle": "", "LocalSerialHostActivityOffline": "", "LocalSerialHostActivityOff": "" }, "Wiz": { "SelectedItemId": "", "Brightness": 100, "IsEnabled": false, "UseActivityStatus": false, "Statuses": { "AvailabilityAvailableStatus": { "Disabled": false, "Color": "#00ff55" }, "AvailabilityAvailableIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityAwayStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBeRightBackStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityBusyStatus": { "Disabled": false, "Color": "#FF3300" }, "AvailabilityBusyIdleStatus": { "Disabled": false, "Color": "#FFFF00" }, "AvailabilityDoNotDisturbStatus": { "Disabled": false, "Color": "#B03CDE" }, "AvailabilityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "AvailabilityOffStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityAvailableStatus": { "Disabled": false, "Color": "#4f824f" }, "ActivityAwayStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBeRightBackStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityBusyStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityDoNotDisturbStatus": { "Disabled": false, "Color": "#960000" }, "ActivityInACallStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityInAConferenceCallStatus": { "Disabled": false, "Color": "#ff00d4" }, "ActivityInactiveStatus": { "Disabled": false, "Color": "#ffff00" }, "ActivityInAMeetingStatus": { "Disabled": false, "Color": "#ff0000" }, "ActivityOfflineStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOffWorkStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityOutOfOfficeStatus": { "Disabled": false, "Color": "#ae00ff" }, "ActivityPresenceUnknownStatus": { "Disabled": false, "Color": "#FFFFFF" }, "ActivityPresentingStatus": { "Disabled": false, "Color": "#960000" }, "ActivityUrgentInterruptionsOnlyStatus": { "Disabled": false, "Color": "#560061" }, "ActivityOffStatus": { "Disabled": false, "Color": "#FFFFFF" } } } } } ================================================ FILE: src/PresenceLight.Web/Program.cs ================================================ using System.Diagnostics; using Blazorise; using Blazorise.Bootstrap; using Blazorise.Icons.FontAwesome; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Options; using Microsoft.Identity.Web; using Microsoft.Identity.Web.UI; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using PresenceLight.Core; using PresenceLight.Razor; using PresenceLight.Razor.Components; using PresenceLight.Web; using Serilog; //var app = ProgramNew.GetWebApplication(args); var app = ProgramOld.GetWebApplication(args); app.Run(); ================================================ FILE: src/PresenceLight.Web/Program_New.cs ================================================ using System.Diagnostics; using Blazorise; using Blazorise.Bootstrap; using Blazorise.Icons.FontAwesome; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Identity.Web; using Microsoft.Identity.Web.UI; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using PresenceLight.Core; using Serilog; namespace PresenceLight.Web { public static class ProgramNew { public static WebApplication GetWebApplication(string[] args) { var builder = WebApplication.CreateBuilder(args); WebApplication app = null; ConfigurationBuilder configBuilderForMain = new ConfigurationBuilder(); configBuilderForMain .SetBasePath(Directory.GetCurrentDirectory()) .AddEnvironmentVariables(); ; if (Debugger.IsAttached) { configBuilderForMain.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: false); configBuilderForMain.AddJsonFile("PresenceLightSettings.Development.json", optional: true, reloadOnChange: false); } else { configBuilderForMain.AddJsonFile("PresenceLightSettings.json", optional: false, reloadOnChange: false); configBuilderForMain.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); } if (Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true") { configBuilderForMain.AddJsonFile(System.IO.Path.Combine("config", "appsettings.json"), optional: true, reloadOnChange: false); configBuilderForMain.AddJsonFile(System.IO.Path.Combine("config", "PresenceLightSettings.json"), optional: true, reloadOnChange: false); } configBuilderForMain.Build(); IConfiguration configForMain = configBuilderForMain.Build(); var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); telemetryConfiguration.InstrumentationKey = configForMain["ApplicationInsights:InstrumentationKey"]; Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(configForMain) .WriteTo.PresenceEventsLogSink() .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces, Serilog.Events.LogEventLevel.Error) .Enrich.FromLogContext() .CreateLogger(); builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()); if (Debugger.IsAttached) { builder.Configuration.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: false); builder.Configuration.AddJsonFile("PresenceLightSettings.Development.json", optional: true, reloadOnChange: false); } else { builder.Configuration.AddJsonFile("PresenceLightSettings.json", optional: false, reloadOnChange: false); builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); } if (Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true") { builder.Configuration.AddJsonFile(System.IO.Path.Combine("config", "appsettings.json"), optional: true, reloadOnChange: false); builder.Configuration.AddJsonFile(System.IO.Path.Combine("config", "PresenceLightSettings.json"), optional: true, reloadOnChange: false); } builder.Configuration.AddEnvironmentVariables(); builder.Logging.AddSerilog(); builder.Host.UseSerilog(); var initialScopes = builder.Configuration.GetValue("DownstreamApi:Scopes")?.Split(' '); builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AADSettings")) .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi")) .AddInMemoryTokenCaches(); builder.Services.AddControllersWithViews() .AddMicrosoftIdentityUI(); builder.Services.AddOptions(OpenIdConnectDefaults.AuthenticationScheme) .Configure((options, serviceProvider) => { options.ResponseType = OpenIdConnectResponseType.Code; options.UsePkce = false; options.Authority = $"{builder.Configuration["AADSettings:Instance"]}common/v2.0"; options.Scope.Add("offline_access"); options.Scope.Add("User.Read"); options.TokenValidationParameters = new TokenValidationParameters { // Azure ID tokens give name in "name" NameClaimType = "name", ValidateIssuer = false }; options.Events = new OpenIdConnectEvents { OnAuthenticationFailed = async context => { context.Response.Redirect("/Error"); context.HandleResponse(); }, OnAuthorizationCodeReceived = async context => { context.HandleCodeRedemption(); var loginService = app.Services.GetRequiredService(); var idToken = await loginService .AddUserToTokenCache(context.ProtocolMessage.Code); context.HandleCodeRedemption(null, idToken); }, OnRedirectToIdentityProviderForSignOut = async context => { var loginService = app.Services.GetRequiredService(); await loginService.SignOut(); } }; }); builder.Services.AddHostedService(); builder.Services.AddAuthorization(options => { // By default, all incoming requests will be authorized according to the default policy options.FallbackPolicy = options.DefaultPolicy; }); builder.Services.AddPresenceLight(builder.Configuration); //builder.Services.AddRazorPages(); //builder.Services.AddServerSideBlazor() builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddMicrosoftIdentityConsentHandler(); builder.Services.AddCascadingAuthenticationState(); builder.Services .AddBlazorise(options => { options.Immediate = true; }) .AddBootstrapProviders() .AddFontAwesomeIcons(); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); app = builder.Build(); app.UseForwardedHeaders(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAntiforgery(); app.UseAuthentication(); app.UseAuthorization(); //app.UseEndpoints(endpoints => //{ // endpoints.MapControllers(); // endpoints.MapBlazorHub(); // endpoints.MapFallbackToPage("/_Host"); //}); app.MapRazorComponents() .AddInteractiveServerRenderMode(); return app; } } } ================================================ FILE: src/PresenceLight.Web/Program_Old.cs ================================================ using System.Diagnostics; using Blazorise; using Blazorise.Bootstrap; using Blazorise.Icons.FontAwesome; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Options; using Microsoft.Identity.Web; using Microsoft.Identity.Web.UI; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using PresenceLight.Core; using PresenceLight.Razor; using PresenceLight.Web; using Serilog; namespace PresenceLight.Web { public static class ProgramOld { public static WebApplication GetWebApplication(string[] args) { var builder = WebApplication.CreateBuilder(args); WebApplication app = null; ConfigurationBuilder configBuilderForMain = new ConfigurationBuilder(); configBuilderForMain .SetBasePath(Directory.GetCurrentDirectory()) .AddEnvironmentVariables(); ; if (Debugger.IsAttached) { configBuilderForMain.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: false); configBuilderForMain.AddJsonFile("PresenceLightSettings.Development.json", optional: true, reloadOnChange: false); } else { configBuilderForMain.AddJsonFile("PresenceLightSettings.json", optional: false, reloadOnChange: false); configBuilderForMain.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); } if (Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true") { configBuilderForMain.AddJsonFile(System.IO.Path.Combine("config", "appsettings.json"), optional: true, reloadOnChange: false); configBuilderForMain.AddJsonFile(System.IO.Path.Combine("config", "PresenceLightSettings.json"), optional: true, reloadOnChange: false); } configBuilderForMain.Build(); IConfiguration configForMain = configBuilderForMain.Build(); var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); telemetryConfiguration.InstrumentationKey = configForMain["ApplicationInsights:InstrumentationKey"]; Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(configForMain) .WriteTo.PresenceEventsLogSink() .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces, Serilog.Events.LogEventLevel.Error) .Enrich.FromLogContext() .CreateLogger(); builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()); if (Debugger.IsAttached) { builder.Configuration.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: false); builder.Configuration.AddJsonFile("PresenceLightSettings.Development.json", optional: true, reloadOnChange: false); } else { builder.Configuration.AddJsonFile("PresenceLightSettings.json", optional: false, reloadOnChange: false); builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); } if (Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true") { builder.Configuration.AddJsonFile(System.IO.Path.Combine("config", "appsettings.json"), optional: true, reloadOnChange: false); builder.Configuration.AddJsonFile(System.IO.Path.Combine("config", "PresenceLightSettings.json"), optional: true, reloadOnChange: false); } builder.Configuration.AddEnvironmentVariables(); builder.Logging.AddSerilog(); builder.Host.UseSerilog(); var initialScopes = builder.Configuration.GetValue("DownstreamApi:Scopes")?.Split(' '); builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AADSettings")) .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi")) .AddInMemoryTokenCaches(); builder.Services.AddControllersWithViews() .AddMicrosoftIdentityUI(); builder.Services.AddOptions(OpenIdConnectDefaults.AuthenticationScheme) .Configure((options, serviceProvider) => { options.ResponseType = OpenIdConnectResponseType.Code; options.UsePkce = false; options.Authority = $"{builder.Configuration["AADSettings:Instance"]}common/v2.0"; options.Scope.Add("offline_access"); options.Scope.Add("User.Read"); options.TokenValidationParameters = new TokenValidationParameters { // Azure ID tokens give name in "name" NameClaimType = "name", ValidateIssuer = false }; options.Events = new OpenIdConnectEvents { OnAuthenticationFailed = async context => { context.Response.Redirect("/Error"); context.HandleResponse(); }, OnAuthorizationCodeReceived = async context => { context.HandleCodeRedemption(); var loginService = app.Services.GetRequiredService(); var idToken = await loginService .AddUserToTokenCache(context.ProtocolMessage.Code); context.HandleCodeRedemption(null, idToken); }, OnRedirectToIdentityProviderForSignOut = async context => { var loginService = app.Services.GetRequiredService(); await loginService.SignOut(); } }; }); builder.Services.AddHostedService(); builder.Services.AddAuthorization(options => { // By default, all incoming requests will be authorized according to the default policy options.FallbackPolicy = options.DefaultPolicy; }); builder.Services.AddPresenceLight(builder.Configuration); builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor() .AddMicrosoftIdentityConsentHandler(); builder.Services .AddBlazorise(options => { options.Immediate = true; }) .AddBootstrapProviders() .AddFontAwesomeIcons(); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); app = builder.Build(); app.UseForwardedHeaders(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAntiforgery(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapBlazorHub(); endpoints.MapFallbackToPage("/_Host"); }); return app; } } } ================================================ FILE: src/PresenceLight.Web/Properties/PublishProfiles/FolderProfile.pubxml ================================================  False False True Release Any CPU FileSystem bin\Release\net10.0\publish\ FileSystem ================================================ FILE: src/PresenceLight.Web/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:28919", "sslPort": 44342 } }, "profiles": { "PresenceLight.Web": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5001;http://localhost:5000", "dotnetRunMessages": true }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Docker": { "commandName": "Docker", "launchBrowser": false, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "environmentVariables": { "ASPNETCORE_URLS": "https://+:443;http://+:80", "ASPNETCORE_HTTPS_PORT": "5001" }, "httpPort": 5000, "useSSL": true, "sslPort": 5001 } } } ================================================ FILE: src/PresenceLight.Web/Properties/serviceDependencies.json ================================================ { "dependencies": { "secrets1": { "type": "secrets" }, "identityapp.aad1": { "type": "identityapp.aad", "connectionId": "AzureAD:ClientSecret" } } } ================================================ FILE: src/PresenceLight.Web/Properties/serviceDependencies.local.json ================================================ { "dependencies": { "secrets1": { "type": "secrets.user" }, "identityapp.aad1": { "type": "identityapp.aad.callsgraph", "connectionId": "AzureAD:ClientSecret", "secretStore": "LocalSecretsFile" } } } ================================================ FILE: src/PresenceLight.Web/Routes.razor ================================================  ================================================ FILE: src/PresenceLight.Web/ServiceCollectionExtensions.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using MediatR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MudBlazor.Services; using PresenceLight.Core; using PresenceLight.Razor; using PresenceLight.Razor.Services; using PresenceLight.Razor.Components; namespace PresenceLight.Web { public static class ServiceCollectionExtensions { public static IServiceCollection AddPresenceLight(this IServiceCollection services, IConfiguration Configuration) { services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(typeof(PresenceLightClientApp).Assembly); cfg.RegisterServicesFromAssembly(typeof(BaseConfig).Assembly); }); services.AddMudServices(); services.AddHttpClient(); services.AddHttpContextAccessor(); services.Configure(Configuration); services.AddSingleton(); services.AddOptions(); services.AddSingleton(); services.AddSingleton(); services.AddPresenceServices(); return services; } } } ================================================ FILE: src/PresenceLight.Web/Worker.cs ================================================ using System.Text.RegularExpressions; using Microsoft.Extensions.Options; using Microsoft.Graph; using Microsoft.Graph.Models; using PresenceLight.Core; namespace PresenceLight.Web { public class Worker : BackgroundService { private readonly AppState _appState; private readonly ILogger _logger; LoginService loginService; private MediatR.IMediator _mediator; public Worker(ILogger logger, IOptionsMonitor optionsAccessor, AppState appState, LoginService _loginService, MediatR.IMediator mediator) { _mediator = mediator; loginService = _loginService; _logger = logger; _appState = appState; _appState.Config = optionsAccessor.CurrentValue; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { if (await loginService.IsUserAuthenticated()) { _logger.LogInformation("User is Authenticated, starting worker"); try { await Run(); } catch (Exception e) { _logger.LogError(e, "Exception occurred restarting worker"); } } else { _logger.LogInformation("User is Not Authenticated, restarting worker"); } await Task.Delay(1000, stoppingToken); } } private async Task Run() { try { if (!await _mediator.Send(new Core.GraphServices.GetIsInitializedCommand())) { await _mediator.Send(new Core.GraphServices.InitializeCommand() { }); if (loginService.IsInitialized) { _appState.SignedIn = true; } } var (user, presence) = await GetUserAndPresence(); var photo = await GetPhotoAsBase64Async(); //Attach properties to all logging within this context.. using (Serilog.Context.LogContext.PushProperty("Availability", presence.Availability)) using (Serilog.Context.LogContext.PushProperty("Activity", presence.Activity)) { _appState.SetUserInfo(user, presence, photo); await SetColor(_appState.Presence.Availability, _appState.Presence.Activity); await InteractWithLights(); } } catch (Exception e) { _logger.LogError(e, "Exception occurred in running worker"); throw; } } private async Task InteractWithLights() { bool previousWorkingHours = false; while (await loginService.IsUserAuthenticated()) { bool useWorkingHours = await _mediator.Send(new Core.WorkingHoursServices.UseWorkingHoursCommand()); bool IsInWorkingHours = await _mediator.Send(new Core.WorkingHoursServices.IsInWorkingHoursCommand()); try { await Task.Delay(Convert.ToInt32(_appState.Config.LightSettings.PollingInterval * 1000)); bool touchLight = false; string newColor = ""; if (_appState.Config.LightSettings.SyncLights) { if (!useWorkingHours) { if (_appState.LightMode == "Graph") { touchLight = true; } } else { if (IsInWorkingHours) { previousWorkingHours = IsInWorkingHours; if (_appState.LightMode == "Graph") { touchLight = true; } } else { // check to see if working hours have passed if (previousWorkingHours) { switch (_appState.Config.LightSettings.HoursPassedStatus) { case "Keep": break; case "White": newColor = "Offline"; break; case "Off": newColor = "Off"; break; default: break; } touchLight = true; } } } } if (touchLight) { switch (_appState.LightMode) { case "Graph": _logger.LogInformation("PresenceLight Running in Teams Mode"); _appState.Presence = await System.Threading.Tasks.Task.Run(() => GetPresence()); if (newColor == string.Empty) { await SetColor(_appState.Presence.Availability, _appState.Presence.Activity); } else { await SetColor(newColor, newColor); } break; default: break; } } } catch (Exception e) { _logger.LogError(e, "Error Occurred Interacting with Lights"); } } } private async Task<(User, Presence)> GetUserAndPresence() { try { var (profile, presence) = await _mediator.Send(new Core.GraphServices.GetProfileAndPresenceCommand()); _logger.LogInformation($"User is {profile.DisplayName}"); return (profile, presence); } catch (Exception ex) { _logger.LogError(ex, "Exception getting me"); throw; } } private async Task GetPhotoAsBase64Async() { try { var photoStream = await _mediator.Send(new Core.GraphServices.GetPhotoCommand()); var memoryStream = new MemoryStream(); photoStream.CopyTo(memoryStream); var photoBytes = memoryStream.ToArray(); var base64Photo = $"data:image/gif;base64,{Convert.ToBase64String(photoBytes)}"; return base64Photo; } catch (Exception ex) { _logger.LogError(ex, "Exception getting photo"); return null; } } public async Task GetPresence() { try { return await _mediator.Send(new Core.GraphServices.GetPresenceCommand()); } catch (Exception e) { _logger.LogError(e, "Error occurred Getting Presence"); throw; } } private async Task SetColor(string color, string activity = null) { try { if (_appState.Config.LightSettings.Hue.IsEnabled) { if (Helpers.AreStringsNotEmpty(new string[] {_appState.Config.LightSettings.Hue.HueApiKey, _appState.Config.LightSettings.Hue.HueIpAddress, _appState.Config.LightSettings.Hue.SelectedItemId })) { if (_appState.Config.LightSettings.Hue.UseRemoteApi) { if (!string.IsNullOrEmpty(_appState.Config.LightSettings.Hue.RemoteBridgeId)) { await _mediator.Send(new Core.RemoteHueServices.SetColorCommand { Availability = color, Activity = activity, LightId = _appState.Config.LightSettings.Hue.SelectedItemId, BridgeId = _appState.Config.LightSettings.Hue.RemoteBridgeId }); } } else { await _mediator.Send(new Core.HueServices.SetColorCommand() { Activity = activity, Availability = color, LightID = _appState.Config.LightSettings.Hue.SelectedItemId }); } } } if (_appState.Config.LightSettings.LIFX.IsEnabled && !string.IsNullOrEmpty(_appState.Config.LightSettings.LIFX.LIFXApiKey)) { await _mediator.Send(new Core.LifxServices.SetColorCommand() { Availability = color, Activity = activity, LightId = _appState.Config.LightSettings.LIFX.SelectedItemId }); } if (_appState.Config.LightSettings.Yeelight.IsEnabled && !string.IsNullOrEmpty(_appState.Config.LightSettings.Yeelight.SelectedItemId)) { await _mediator.Send(new PresenceLight.Core.YeelightServices.SetColorCommand { Activity = activity, Availability = color, LightId = _appState.Config.LightSettings.Yeelight.SelectedItemId }); } if (_appState.Config.LightSettings.CustomApi.IsEnabled) { string response = await _mediator.Send(new Core.CustomApiServices.SetColorCommand { Activity = activity, Availability = color }); } if (_appState.Config.LightSettings.LocalSerialHost.IsEnabled && !string.IsNullOrEmpty(_appState.Config.LightSettings.LocalSerialHost.SelectedItemId)) { string response = await _mediator.Send(new Core.LocalSerialHostServices.SetColorCommand { Activity = activity, Availability = color }); } if (_appState.Config.LightSettings.Wiz.IsEnabled) { //await _mediator.Send(new Core.WizServices.SetColorCommand //{ // Activity = activity, // Availability = color, // LightID = _appState.Config.LightSettings.Wiz.SelectedItemId //}); } } catch (Exception e) { _logger.LogError(e, "Exception setting color"); throw; } } } } ================================================ FILE: src/PresenceLight.Web/_Imports.razor ================================================ @using Microsoft.AspNetCore.Components.Authorization @using PresenceLight.Razor.Components @using PresenceLight.Razor.Components.Layout @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web ================================================ FILE: src/PresenceLight.Web/appsettings.json ================================================ { "AADSettings": { "Instance": "https://login.microsoftonline.com/", "TenantId": "", "ClientId": "", "RedirectHost": "https://localhost:5001", "CallbackPath": "/signin-oidc", "SignedOutCallbackPath ": "/signout-callback-oidc", "ClientSecret": "", "Scopes": [ "https://graph.microsoft.com/.default" ] }, "DownstreamApi": { "BaseUrl": "https://graph.microsoft.com/beta", "Scopes": "user.read presence.read offline_access" }, "ApplicationInsights": { "InstrumentationKey": "" }, "SnapshotCollectorConfiguration": { "IsEnabledInDeveloperMode": true, "ThresholdForSnapshotting": 1, "MaximumSnapshotsRequired": 3, "MaximumCollectionPlanSize": 50, "ReconnectInterval": "00:15:00", "ProblemCounterResetInterval": "1.00:00:00", "SnapshotsPerTenMinutesLimit": 1, "SnapshotsPerDayLimit": 30, "SnapshotInLowPriorityThread": true, "ProvideAnonymousTelemetry": true, "FailedRequestLimit": 3 }, "Serilog": { "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], "MinimumLevel": "Information", "WriteTo": [ { "Name": "Console" }, { "Name": "File", "Args": { "path": "config/logs/log-.json", "formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog", "rollingInterval": "Hour", "shared": "true", "retainedFileCountLimit": 24 } } ], "Enrich": [ "FromLogContext", "WithThreadId" ], "Properties": { "Application": "PresenceLight" } }, "AppType": "Web", "AppVersion": "" } ================================================ FILE: src/PresenceLight.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31717.71 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PresenceLight.Core", "PresenceLight.Core\PresenceLight.Core.csproj", "{4883809C-FF4B-4504-A85E-2503607E5B99}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PresenceLight", "DesktopClient\PresenceLight\PresenceLight.csproj", "{E0C9DB61-A1E6-4254-A200-A946F4353D66}" EndProject Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "PresenceLight.Package", "DesktopClient\PresenceLight.Package\PresenceLight.Package.wapproj", "{12DDAD24-CCBD-409F-9342-17A0E445604F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9BCEC868-73D3-4393-8099-D4DF2DC6DB74}" ProjectSection(SolutionItems) = preProject ..\.gitignore = ..\.gitignore EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Readmes", "Readmes", "{AEA7D3CB-0200-4256-BB51-D9A63E788C97}" ProjectSection(SolutionItems) = preProject ..\docs\desktop-README.md = ..\docs\desktop-README.md ..\README.md = ..\README.md ..\docs\web-README.md = ..\docs\web-README.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PresenceLight.Razor", "PresenceLight.Razor\PresenceLight.Razor.csproj", "{0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PresenceLight.Web", "PresenceLight.Web\PresenceLight.Web.csproj", "{F091F5B7-84B4-48B7-A2DB-862A351F4D0A}" EndProject Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "DockerCompose\docker-compose.dcproj", "{D6871E74-A6E2-41EE-AA4A-7BE357501D63}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub Workflows", "GitHub Workflows", "{E1FC1E76-1B1A-411B-B5A1-42446709144B}" ProjectSection(SolutionItems) = preProject ..\.github\workflows\Azure_Blob_Deploy.yml = ..\.github\workflows\Azure_Blob_Deploy.yml ..\.github\workflows\Choco.yml = ..\.github\workflows\Choco.yml ..\.github\workflows\Deploy_Desktop.yml = ..\.github\workflows\Deploy_Desktop.yml ..\.github\workflows\Deploy_Web.yml = ..\.github\workflows\Deploy_Web.yml ..\.github\workflows\Sign.yml = ..\.github\workflows\Sign.yml ..\.github\workflows\WinGet.yml = ..\.github\workflows\WinGet.yml EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU Release|ARM64 = Release|ARM64 Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4883809C-FF4B-4504-A85E-2503607E5B99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Debug|Any CPU.Build.0 = Debug|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Debug|ARM64.ActiveCfg = Debug|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Debug|ARM64.Build.0 = Debug|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Debug|x64.ActiveCfg = Debug|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Debug|x64.Build.0 = Debug|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Release|Any CPU.ActiveCfg = Release|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Release|Any CPU.Build.0 = Release|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Release|ARM64.ActiveCfg = Release|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Release|ARM64.Build.0 = Release|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Release|x64.ActiveCfg = Release|Any CPU {4883809C-FF4B-4504-A85E-2503607E5B99}.Release|x64.Build.0 = Release|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Debug|Any CPU.Build.0 = Debug|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Debug|ARM64.ActiveCfg = Debug|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Debug|ARM64.Build.0 = Debug|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Debug|x64.ActiveCfg = Debug|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Debug|x64.Build.0 = Debug|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Release|Any CPU.Build.0 = Release|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Release|ARM64.ActiveCfg = Release|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Release|ARM64.Build.0 = Release|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Release|x64.ActiveCfg = Release|Any CPU {E0C9DB61-A1E6-4254-A200-A946F4353D66}.Release|x64.Build.0 = Release|Any CPU {12DDAD24-CCBD-409F-9342-17A0E445604F}.Debug|Any CPU.ActiveCfg = Debug|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Debug|Any CPU.Build.0 = Debug|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Debug|Any CPU.Deploy.0 = Debug|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Debug|ARM64.ActiveCfg = Debug|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Debug|ARM64.Build.0 = Debug|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Debug|ARM64.Deploy.0 = Debug|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Debug|x64.ActiveCfg = Debug|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Debug|x64.Build.0 = Debug|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Debug|x64.Deploy.0 = Debug|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Release|Any CPU.ActiveCfg = Release|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Release|Any CPU.Build.0 = Release|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Release|Any CPU.Deploy.0 = Release|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Release|ARM64.ActiveCfg = Release|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Release|ARM64.Build.0 = Release|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Release|ARM64.Deploy.0 = Release|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Release|x64.ActiveCfg = Release|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Release|x64.Build.0 = Release|x64 {12DDAD24-CCBD-409F-9342-17A0E445604F}.Release|x64.Deploy.0 = Release|x64 {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Debug|Any CPU.Build.0 = Debug|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Debug|ARM64.ActiveCfg = Debug|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Debug|ARM64.Build.0 = Debug|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Debug|x64.ActiveCfg = Debug|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Debug|x64.Build.0 = Debug|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Release|Any CPU.Build.0 = Release|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Release|ARM64.ActiveCfg = Release|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Release|ARM64.Build.0 = Release|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Release|x64.ActiveCfg = Release|Any CPU {0D9ADF1D-606A-4FD1-8EA1-7619EAE16703}.Release|x64.Build.0 = Release|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Debug|Any CPU.Build.0 = Debug|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Debug|ARM64.ActiveCfg = Debug|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Debug|ARM64.Build.0 = Debug|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Debug|x64.ActiveCfg = Debug|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Debug|x64.Build.0 = Debug|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Release|Any CPU.ActiveCfg = Release|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Release|Any CPU.Build.0 = Release|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Release|ARM64.ActiveCfg = Release|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Release|ARM64.Build.0 = Release|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Release|x64.ActiveCfg = Release|Any CPU {F091F5B7-84B4-48B7-A2DB-862A351F4D0A}.Release|x64.Build.0 = Release|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Debug|Any CPU.Build.0 = Debug|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Debug|ARM64.ActiveCfg = Debug|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Debug|ARM64.Build.0 = Debug|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Debug|x64.ActiveCfg = Debug|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Debug|x64.Build.0 = Debug|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Release|Any CPU.Build.0 = Release|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Release|ARM64.ActiveCfg = Release|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Release|ARM64.Build.0 = Release|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Release|x64.ActiveCfg = Release|Any CPU {D6871E74-A6E2-41EE-AA4A-7BE357501D63}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {AEA7D3CB-0200-4256-BB51-D9A63E788C97} = {9BCEC868-73D3-4393-8099-D4DF2DC6DB74} {E1FC1E76-1B1A-411B-B5A1-42446709144B} = {9BCEC868-73D3-4393-8099-D4DF2DC6DB74} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B89FF65-2B56-495E-8EA6-2B279DA440AE} EndGlobalSection EndGlobal ================================================ FILE: version.json ================================================ { "version": "6.0", "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/develop$", "^refs/heads/rel/v\\d+\\.\\d+" ] }