diff --git a/.github/workflows/actions/app-build/Set-Version.ps1 b/.github/workflows/actions/app-build/Set-Version.ps1 index d72409e2..c3a8f4af 100644 --- a/.github/workflows/actions/app-build/Set-Version.ps1 +++ b/.github/workflows/actions/app-build/Set-Version.ps1 @@ -1,3 +1,17 @@ +<# +.Synopsis + Sets the assembly version of all assemblies in the source directory. +.DESCRIPTION + A build script that can be included in TFS 2015 or Visual Studio Online (VSO) vNevt builds that update the version of all assemblies in a workspace. + It uses the name of the build to extract the version number and updates all AssemblyInfo.cs files to use the new version. +.EXAMPLE + Set-AssemblyVersion +.EXAMPLE + Set-AssemblyVersion -SourceDirectory $Env:BUILD_SOURCESDIRECTORY -BuildNumber $Env:BUILD_BUILDNUMBER +.EXAMPLE + Set-AssemblyVersion -SourceDirectory ".\SourceDir" -BuildNumber "Dev_1.0.20150922.01" -VersionFormat "\d+\.\d+\.\d+\.\d+" +#> + [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] [Alias()] [OutputType([int])] @@ -32,6 +46,19 @@ Param [string]$VersionFormat = "\d+\.\d+\.\d+\.\d+" ) +<# +.Synopsis + Sets the assembly version of all assemblies in the source directory. +.DESCRIPTION + A build script that can be included in TFS 2015 or Visual Studio Online (VSO) vNevt builds that update the version of all assemblies in a workspace. + It uses the name of the build to extract the version number and updates all AssemblyInfo.cs files to use the new version. +.EXAMPLE + Set-AssemblyVersion +.EXAMPLE + Set-AssemblyVersion -SourceDirectory $Env:BUILD_SOURCESDIRECTORY -BuildNumber $Env:BUILD_BUILDNUMBER +.EXAMPLE + Set-AssemblyVersion -SourceDirectory ".\SourceDir" -BuildNumber "Dev_1.0.20150922.01" -VersionFormat "\d+\.\d+\.\d+\.\d+" +#> function Set-AssemblyVersion { [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] @@ -92,12 +119,18 @@ function Set-AssemblyVersion } Write-Host "Update BUILD_BUILDNUMBER to $($UpdatedVersion.ToString())" - Write-Host "BUILD_NUMBER=$($UpdatedVersion.ToString())" >> $GITHUB_ENV + echo "BUILD_NUMBER=$($UpdatedVersion.ToString())" >> $GITHUB_ENV - if($SetVersion.IsPresent){ + if(-not $SetVersion.IsPresent){ + Write-Host "Not able to parse version number." + return + } + + if($files -and $files.Length -lt 0){ Set-FileContent -Files $files -Version $UpdatedVersion -VersionFormat $VersionFormat - Set-AppManifest -Files $appFiles -Version $UpdatedVersion - } + } + + Set-AppManifest -Files $appFiles -Version $UpdatedVersion } function Create-NewVersionNumber{ diff --git a/.github/workflows/actions/app-build/action.yml b/.github/workflows/actions/app-build/action.yml index 409c3704..7ca73cb5 100644 --- a/.github/workflows/actions/app-build/action.yml +++ b/.github/workflows/actions/app-build/action.yml @@ -52,4 +52,4 @@ runs: SONAR_TOKEN: ${{inputs.SONAR_TOKEN}} run: | Write-Host "==== Build app package ====" -ForegroundColor Green - msbuild.exe ${{github.workspace}}\repo\OpenHAB.Windows.sln /p:Platform="x86" /p:AppxBundlePlatforms="${{inputs. bundle_Platforms}}" /p:AppxPackageDir="${{inputs.output_directory}}" /p:AppxBundle=Always /p:UapAppxPackageBuildMode=StoreUpload /p:configuration="${{inputs.build_configuration}}" /t:rebuild \ No newline at end of file + msbuild.exe ${{github.workspace}}\repo\OpenHAB.Windows.sln /p:Platform="x86" /p:AppxBundlePlatforms="${{inputs.bundle_Platforms}}" /p:AppxPackageDir="${{inputs.output_directory}}" /p:AppxBundle=Always /p:UapAppxPackageBuildMode=StoreUpload /p:configuration="${{inputs.build_configuration}}" /t:rebuild \ No newline at end of file diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml deleted file mode 100644 index ea244077..00000000 --- a/.github/workflows/app-release.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: App Release - -env: - BUILD_NUMBER: '1970.1.1' - BuildDirectory: '${{github.workspace}}\build' - ReleaseName: 'N/A' - IsBetaRelease: ${{ contains(github.event.inputs.BetaRelease, 'true') }} - -on: - workflow_dispatch: - inputs: - BetaRelease: - description: 'Specifices if a beta release should be created.' - default: 'false' - required: false - BuildConfiguration: - description: 'Specifices if a release or debug package should be created.' - default: 'release' - required: false - -jobs: - build: - name: 'Build openHAB Windows App' - runs-on: windows-latest - - steps: - - name: Checkout main code - uses: actions/checkout@v2 - with: - path: repo - clean: true - - - name: Setup NuGet.exe for use with actions - uses: NuGet/setup-nuget@v1.0.5 - with: - nuget-version: 'latest' - - - name: setup-msbuild - uses: microsoft/setup-msbuild@v1 - - - name: Build App - id: create_app_package - uses: ./repo/.github/workflows/actions/app-build - with: - build_configuration: ${{github.event.inputs.BuildConfiguration}} - output_directory: '${{env.BuildDirectory}}' - - - name: Upload build assets - id: app_package_upload - uses: actions/upload-artifact@v2.2.2 - with: - name: app - path: '${{env.BuildDirectory}}' - - release: - name: 'Creates an app release' - runs-on: ubuntu-latest - needs: build - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/releases/beta' - steps: - - name: Set build number and release name - id: set-buildnumbe - run: | - CURRENTDATE=`date +"%Y.%m.%d"` - echo "BUILD_NUMBER=${CURRENTDATE}.${{github.run_number}}" >> $GITHUB_ENV - - IS_BETA_RELEASE= ${{ env.IsBetaRelease }} - if[$IS_BETA_RELEASE]; then - echo "ReleaseName=Beta:${CURRENTDATE}.${{github.run_number}}" >> $GITHUB_ENV - else: - echo "ReleaseName=${CURRENTDATE}.${{github.run_number}}" >> $GITHUB_ENV - fi - - - name: Create a Release - id: create_release - uses: actions/create-release@v1.1.4 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{env.BUILD_NUMBER}} - release_name: ${{env.ReleaseName}} - - - name: Download a App Package from Build Artifacts - uses: actions/download-artifact@v2.0.8 - with: - name: app - path: '${{env.BuildDirectory}}' - - - name: Upload Release App Package - id: upload-release-package - uses: actions/upload-release-asset@v1.0.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{steps.create_release.outputs.upload_url}} - asset_path: '${{env.BuildDirectory}}' - asset_name: OpenHab UWP App diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..58960bf2 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,125 @@ +name: 'Build App Package' + +on: + workflow_call: + inputs: + build_configuration: + description: 'The build configuration to use' + type: string + required: true + default: 'Release' + bundle_Platforms: + type: string + description: 'Defines the platforms for the app package.' + required: false + default: ' x86|x64|arm64' + build_number: + type: string + description: 'The build number to use' + required: true + +jobs: + build: + name: 'Build App Package' + runs-on: windows-latest + env: + output_directory: ${{github.workspace}}\build + steps: + - name: Checkout main code + uses: actions/checkout@v2 + with: + path: repo + clean: true + + - name: Update Version Number in app files + id: update-packageverion + shell: pwsh + run: | + $version = [System.Version]::Parse('${{inputs.build_number}}') + ${{github.workspace}}\repo\.github\workflows\scripts\Set-Version.ps1 -SourceDirectory '${{github.workspace}}/repo/src/' -Major $version.Major -Minor $version.Minor -Build $version.Build -Revision 0 -SetVersion + + - name: Setup NuGet.exe for use with actions + uses: NuGet/setup-nuget@v1.0.5 + with: + nuget-version: 'latest' + + - name: setup-msbuild + uses: microsoft/setup-msbuild@v2 + + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.x.x' + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'zulu' + + - name: Cache SonarQube Cloud packages + uses: actions/cache@v4 + with: + path: ~\sonar\cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: Cache SonarCloud scanner + id: cache-sonar-scanner + uses: actions/cache@v3 + with: + path: .\.sonar\scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + + - name: Install SonarQube Cloud scanner + if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' + shell: powershell + run: | + New-Item -Path .\.sonar\scanner -ItemType Directory + dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner + + - name: Begin SonarQube analyze + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + SONAR_TOKEN: ${{secrets.SONAR_TOKEN}} + shell: pwsh + run: | + .\.sonar\scanner\dotnet-sonarscanner begin /k:"openhab_openhab-windows" /o:"openhab" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" + + - name: Restore nuget packages + shell: pwsh + run: | + dotnet restore ${{github.workspace}}\repo\OpenHAB.Windows.sln --force --no-cache + + - name: Build OpenHab Windows App + shell: pwsh + run: | + msbuild.exe ${{github.workspace}}\repo\OpenHAB.Windows.sln ` + /p:Platform="x86" ` + /p:AppxBundlePlatforms="${{inputs.bundle_Platforms}}" ` + /p:AppxPackageDir="${{env.output_directory}}" ` + /p:AppxBundle=Always ` + /p:UapAppxPackageBuildMode=StoreUpload ` + /p:Configuration="${{inputs.build_configuration}}" ` + /t:Rebuild + + - name: End SonarQube analyze + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + SONAR_TOKEN: ${{secrets.SONAR_TOKEN}} + shell: pwsh + run: | + .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + + - name: List files in output directory + shell: pwsh + working-directory: ${{env.output_directory}} + run: | + Get-ChildItem -Path ${{env.output_directory}} + + - name: Upload App Package + uses: actions/upload-artifact@v4 + with: + name: app + path: ${{env.output_directory}} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2316b11c..cfe5e4ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,85 +1,65 @@ -name: 'CI Build' +name: 'openHAB Build CI Build' +run-name: app-ci-${{github.run_id }} env: - BUILD_NUMBER: '1970.1.1' BuildDirectory: '${{github.workspace}}\build' - ACTIONS_RUNNER_DEBUG: true -on: [push, pull_request] +on: + pull_request: + branches: + - main jobs: - build: - name: 'Build openHAB Windows App' - runs-on: windows-latest - + configure: + name: Configure Build + runs-on: ubuntu-latest + outputs: + buildConfiguration: ${{ steps.configuration.outputs.BuildConfiguration }} + bundlePlatforms: ${{ steps.configuration.outputs.BundlePlatforms }} + buildNumber: ${{ steps.buildnumber.outputs.BUILD_NUMBER }} + releaseName: ${{ steps.buildnumber.outputs.RELEASE_NAME }} steps: - - name: Checkout main code - uses: actions/checkout@v2 - with: - path: repo - clean: true - - - name: Setup NuGet.exe for use with actions - uses: NuGet/setup-nuget@v1.0.5 - with: - nuget-version: 'latest' - - - name: setup-msbuild - uses: microsoft/setup-msbuild@v1 + - name: Checkout repository + uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 1.11 + - name: Detect Build Configuration + id: configuration + shell: pwsh + run: | + if ($env:github.ref -eq 'refs/heads/main') { + Write-Host "Set build configuration to release" - - name: Cache SonarCloud packages - uses: actions/cache@v1 - with: - path: ~\sonar\cache - key: ${{runner.os}}-sonar - restore-keys: ${{runner.os}}-sonar + Write-Output "BuildConfiguration=release" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + Write-Output "BundlePlatforms=x86|x64|arm64" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + } else { + Write-Host "Set build configuration to debug" - - name: Cache SonarCloud scanner - id: cache-sonar-scanner - uses: actions/cache@v1 - with: - path: .\.sonar\scanner - key: ${{runner.os}}-sonar-scanner-msbuild - restore-keys: ${{runner.os}}-sonar-scanner-msbuild + Write-Output "BuildConfiguration=debug" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + Write-Output "BundlePlatforms=x86|x64|arm64" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + } - - name: Install SonarCloud scanner - if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' - shell: pwsh - run: | - $directory = New-Item -Path .\.sonar\scanner -ItemType Directory - # dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner + - name: Set build number and release name + id: buildnumber + shell: pwsh + run: | + $CURRENTDATE = Get-Date -Format "yyyy.MM.dd" + Write-Output "BUILD_NUMBER=$CURRENTDATE" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - $file = Join-Path $directory.FullName "sonar-scanner.zip" - Invoke-WebRequest -Uri "https://github.com/SonarSource/sonar-scanner-msbuild/releases/download/5.2.1.31210/sonar-scanner-msbuild-5.2.1.31210-net46.zip" -OutFile $file - Expand-Archive -LiteralPath $file -DestinationPath "$($directory.FullName)" + $IS_BETA_RELEASE = '${{ inputs.IsBetaRelease }}' + if ($IS_BETA_RELEASE -eq 'true') { + Write-Host "Set release name to Beta:$CURRENTDATE.${{github.run_number}}" + Write-Output "RELEASE_NAME=Beta:$CURRENTDATE.${{github.run_number}}" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + } else { + Write-Host "Set release name to $CURRENTDATE" + Write-Output "RELEASE_NAME=$CURRENTDATE" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + } - - name: Begin SonarQube analyze - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - SONAR_TOKEN: ${{secrets.SONAR_TOKE }} - shell: pwsh - run: | - .\.sonar\scanner\SonarScanner.MSBuild.exe begin /k:"openhab_openhab-windows" /o:"openhab" /d:sonar.login="${{secrets.SONAR_TOKEN}}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.projectBaseDir="${{github.workspace}}/repo/src/" /d:sonar.verbose="true" - - - name: Build App - id: create_app_package - uses: ./repo/.github/workflows/actions/app-build - with: - build_configuration: 'debug' - output_directory: '${{env.BuildDirectory}}' - bundle_Platforms: 'x86' - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - SONAR_TOKEN: ${{secrets.SONAR_TOKEN}} - - - name: End SonarQube analyze - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - SONAR_TOKEN: ${{secrets.SONAR_TOKEN}} - shell: pwsh - run: | - .\.sonar\scanner\SonarScanner.MSBuild.exe end /d:sonar.login="${{secrets.SONAR_TOKEN}}" \ No newline at end of file + build: + uses: ./.github/workflows/build.yml + name: Build + needs: configure + with: + build_configuration: ${{needs.configure.outputs.buildConfiguration}} + bundle_Platforms: ${{ needs.configure.outputs.bundlePlatforms}} + build_number: ${{ needs.configure.outputs.buildNumber}} + secrets: inherit diff --git a/.github/workflows/openhab.yml b/.github/workflows/openhab.yml new file mode 100644 index 00000000..f97a1d2b --- /dev/null +++ b/.github/workflows/openhab.yml @@ -0,0 +1,117 @@ +name: "openHAB Build and Release app" +run-name: run-${{github.head_ref || github.ref_name}}-${{github.run_number}} + +env: + BuildDirectory: '${{github.workspace}}\build' + ACTIONS_RUNNER_DEBUG: true + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + BuildConfiguration: + description: "Specifies if a release or debug package should be created." + type: choice + options: ["release", "debug"] + default: "release" + IsBetaRelease: + description: "Create store beta release." + type: "boolean" + default: false + create_release: + description: "Create release for non-main branch." + type: "boolean" + default: false + releaseName: + description: "The release name to use." + type: "string" + default: "" + +jobs: + configure: + name: Configure Build + runs-on: ubuntu-latest + outputs: + buildConfiguration: ${{ steps.configuration.outputs.BuildConfiguration }} + bundlePlatforms: ${{ steps.configuration.outputs.BundlePlatforms }} + buildNumber: ${{ steps.buildnumber.outputs.BUILD_NUMBER }} + releaseName: ${{ steps.buildnumber.outputs.RELEASE_NAME }} + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Detect Build Configuration + id: configuration + shell: pwsh + run: | + if ($env:github.ref -eq 'refs/heads/main') { + Write-Host "Set build configuration to release" + + #Write-Output "BuildConfiguration=release" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + Write-Output "BundlePlatforms=x86|x64|arm64" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + } else { + Write-Host "Set build configuration to debug" + + #Write-Output "BuildConfiguration=debug" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + Write-Output "BundlePlatforms=x86|x64|arm64" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + } + + - name: Set build number and release name + id: buildnumber + shell: pwsh + run: | + $CURRENTDATE = Get-Date -Format "yyyy.MM.dd" + Write-Output "BUILD_NUMBER=$CURRENTDATE" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + + $IS_BETA_RELEASE = '${{ inputs.IsBetaRelease }}' + if ($IS_BETA_RELEASE -eq 'true') { + Write-Host "Set release name to Beta:$CURRENTDATE.${{github.run_number}}" + Write-Output "RELEASE_NAME=Beta:$CURRENTDATE.${{github.run_number}}" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + } else { + Write-Host "Set release name to $CURRENTDATE" + Write-Output "RELEASE_NAME=$CURRENTDATE" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + } + + build: + uses: ./.github/workflows/build.yml + name: Build + needs: configure + with: + build_configuration: ${{ inputs.BuildConfiguration }} #${{needs.configure.outputs.buildConfiguration}} + bundle_Platforms: ${{ needs.configure.outputs.bundlePlatforms}} + build_number: ${{ needs.configure.outputs.buildNumber}} + secrets: inherit + + create_release: + name: Create Release + uses: ./.github/workflows/release.yml + needs: + - build + - configure + with: + force_release: ${{ inputs.create_release }} + beta_release: ${{ inputs.IsBetaRelease }} + build_number: ${{ needs.configure.outputs.buildNumber }} + releaseName: ${{ inputs.releaseName }} + + # publish_beta: + # uses: ./.github/workflows/publish.yml + # needs: + # - build + # - configure + # with: + # environment: BETA + # package_name: "openHAB.Windows_${{needs.configure.outputs.buildNumber}}_x86_x64_arm_bundle" + # secrets: inherit + + # publish_prod: + # uses: ./.github/workflows/publish.yml + # needs: + # - publish_beta + # - configure + # with: + # environment: PROD + # package_name: "openHAB.Windows_${{needs.configure.outputs.buildNumber}}_x86_x64_arm_bundle" + # secrets: inherit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..a6407fdc --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,46 @@ +name: 'Deploy App Package' +run-name: 'Deploy App Package: ${{ github.event.inputs.environment }}' + +env: + BuildDirectory: '${{github.workspace}}\build' + +on: + workflow_call: + inputs: + environment: + type: string + description: 'Defines the environment for the app package' + required: true + package_name: + type: string + description: 'The name of the app package' + required: true + artifact_name: + type: string + description: 'The name of the artifact to download' + default: 'app' + artifact_directory: + type: string + description: 'Defines the output directory for the app package' + default: '${{github.workspace}}/packages/' + +jobs: + deploy: + name: 'Deploy App Package to ${{ github.event.inputs.environment }} channel' + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment }} + steps: + - name: Download a App Package from Build Artifacts + uses: actions/download-artifact@v2.0.8 + with: + name: ${{inputs.artifact_name}} + path: '${{env.BuildDirectory}}/${{inputs.artifact_directory}}/' + + - uses: isaacrlevin/windows-store-action + name: Publish to Store + with: + tenant-id: ${{ secrets.AZURE_AD_TENANT_ID }} + client-id: ${{ secrets.AZURE_AD_APPLICATION_CLIENT_ID }} + client-secret: ${{ secrets.AZURE_AD_APPLICATION_SECRET }} + app-id: ${{ secrets.STORE_APP_ID }} + package-path: "${{ github.workspace }}/${{inputs.artifact_directory}}/${{inputs.package_name}}.msixupload" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ec8e1125 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,80 @@ +name: App Release + +env: + BuildDirectory: '${{github.workspace}}/build' + +on: + workflow_call: + inputs: + force_release: + description: 'Specifies if a release should be created even if the branch is not main.' + type: boolean + default: false + required: false + beta_release: + description: 'Specifies if a beta release should be created.' + type: boolean + default: false + required: false + build_number: + type: string + description: 'The build number to use' + required: true + releaseName: + type: string + description: 'The release name to use' + required: true + +permissions: + id-token: "write" + contents: "write" + packages: "write" + pull-requests: "read" + +jobs: + release: + name: 'App release creation for ${{inputs.build_number}}' + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/releases/beta' || inputs.force_release == true + steps: + - name: Download a App Package from Build Artifacts + uses: actions/download-artifact@v4 + with: + name: app + path: '${{env.BuildDirectory}}' + + - name: Display structure of downloaded files + run: ls -la + working-directory: '${{env.BuildDirectory}}' + + - name: Set release name + run: | + if [ -z "${releaseName}" ]; then + echo "No release name provided, using build number" + release_name="${{ inputs.build_number }}" + else + echo "Using provided release name" + release_name="${releaseName}" + fi + + if [ "${beta_release}" == "true" ]; then + echo "Beta release requested" + release_name="Beta: ${release_name}" + fi + + echo "releaseName=${release_name}" >> $GITHUB_ENV + env: + releaseName: ${{ inputs.releaseName }} + beta_release: ${{ inputs.beta_release }} + + - name: Create a Release + id: create_release + uses: marvinpinto/action-automatic-releases@latest + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + prerelease: ${{inputs.beta_release}} + automatic_release_tag: ${{inputs.build_number}} + title: ${{env.releaseName}} + files: | + ${{env.BuildDirectory}}/**/*.msixbundle + ${{env.BuildDirectory}}/**/*.msixupload diff --git a/.github/workflows/scripts/Set-Version.ps1 b/.github/workflows/scripts/Set-Version.ps1 index 08225aff..c3a8f4af 100644 --- a/.github/workflows/scripts/Set-Version.ps1 +++ b/.github/workflows/scripts/Set-Version.ps1 @@ -121,10 +121,16 @@ function Set-AssemblyVersion Write-Host "Update BUILD_BUILDNUMBER to $($UpdatedVersion.ToString())" echo "BUILD_NUMBER=$($UpdatedVersion.ToString())" >> $GITHUB_ENV - if($SetVersion.IsPresent){ + if(-not $SetVersion.IsPresent){ + Write-Host "Not able to parse version number." + return + } + + if($files -and $files.Length -lt 0){ Set-FileContent -Files $files -Version $UpdatedVersion -VersionFormat $VersionFormat - Set-AppManifest -Files $appFiles -Version $UpdatedVersion - } + } + + Set-AppManifest -Files $appFiles -Version $UpdatedVersion } function Create-NewVersionNumber{ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..a99bcac8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +default_install_hook_types: + - pre-commit + - commit-msg + - pre-push +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - repo: https://github.com/openhab/openhab-windows + rev: 98df98f04814a30ba4df438744d5f68ebcf5d815 + hooks: + - id: sign-off + stages: [commit-msg] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 00000000..a7f3e6d3 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: sign-off + name: Sign-off commits + entry: "pwsh -c 'git interpret-trailers --in-place --trailer \"Signed-off-by: $(git config user.name) <$(git config user.email)>\" .git/COMMIT_EDITMSG'" + language: system + stages: [commit-msg] diff --git a/.sonarlint/OpenHAB.Windows.json b/.sonarlint/OpenHAB.Windows.json new file mode 100644 index 00000000..53391710 --- /dev/null +++ b/.sonarlint/OpenHAB.Windows.json @@ -0,0 +1,4 @@ +{ + "SonarCloudOrganization": "openhab", + "ProjectKey": "openhab_openhab-windows" +} \ No newline at end of file diff --git a/.sonarlint/OpenHAB.Windows.slconfig b/.sonarlint/OpenHAB.Windows.slconfig deleted file mode 100644 index 704efb31..00000000 --- a/.sonarlint/OpenHAB.Windows.slconfig +++ /dev/null @@ -1,15 +0,0 @@ -{ - "ServerUri": "https://sonarcloud.io/", - "Organization": { - "Key": "openhab", - "Name": "openHAB" - }, - "ProjectKey": "openhab_openhab-windows", - "ProjectName": "openhab-windows", - "Profiles": { - "CSharp": { - "ProfileKey": "AXflIL33YGfeNt47qUY1", - "ProfileTimestamp": "2021-11-22T10:44:23Z" - } - } -} \ No newline at end of file diff --git a/.sonarlint/openhab_openhab-windows/CSharp/SonarLint.xml b/.sonarlint/openhab_openhab-windows/CSharp/SonarLint.xml deleted file mode 100644 index 90bc98df..00000000 --- a/.sonarlint/openhab_openhab-windows/CSharp/SonarLint.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - sonar.cs.analyzeGeneratedCode - false - - - sonar.cs.file.suffixes - .cs - - - sonar.cs.ignoreHeaderComments - true - - - sonar.cs.roslyn.ignoreIssues - false - - - - - S107 - - - max - 7 - - - - - S110 - - - max - 5 - - - - - S1479 - - - maximum - 30 - - - - - S2342 - - - flagsAttributeFormat - ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$ - - - format - ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ - - - - - S2436 - - - max - 2 - - - maxMethod - 3 - - - - - S3776 - - - propertyThreshold - 3 - - - threshold - 15 - - - - - \ No newline at end of file diff --git a/.sonarlint/openhab_openhab-windowscsharp.ruleset b/.sonarlint/openhab_openhab-windowscsharp.ruleset deleted file mode 100644 index 79df8173..00000000 --- a/.sonarlint/openhab_openhab-windowscsharp.ruleset +++ /dev/null @@ -1,370 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..9751e827 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,38 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "PowerShell Launch Current File", + "type": "PowerShell", + "request": "launch", + "script": "${file}", + "cwd": "${file}", + "args": [ + "-SourceDirectory", "${workspaceFolder}/src", + "-Major", "2022", + "-Minor","9", + "-Build","12", + "-Revision", "0", + "-SetVersion" + ] + }, + { + "name": "Build: Launch Version Script", + "type": "PowerShell", + "request": "launch", + "script": "./Set-Version.ps1", + "cwd": "${workspaceFolder}/.github/workflows/scripts/", + "args": [ + "-SourceDirectory", "${workspaceFolder}/src", + "-Major", "2022", + "-Minor","9", + "-Build","12", + "-Revision", "0", + "-SetVersion" + ] + } + ] +} \ No newline at end of file diff --git a/OpenHAB.Windows.sln b/OpenHAB.Windows.sln index 7824d181..d8f8265b 100644 --- a/OpenHAB.Windows.sln +++ b/OpenHAB.Windows.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29409.12 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32825.248 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "libaries", "libaries", "{6D9E45E3-F3C1-4A9D-A62D-72DDBA11C31B}" ProjectSection(SolutionItems) = preProject @@ -13,50 +13,118 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Openhab.Core", "src\openHAB.Core\Openhab.Core.csproj", "{2A8FD10E-4EE5-4E25-B47C-2296E071F482}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "openHAB.Core", "src\openHAB.Core\openHAB.Core.csproj", "{2A8FD10E-4EE5-4E25-B47C-2296E071F482}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "openHAB.Windows", "src\openHAB.Windows\openHAB.Windows.csproj", "{D65E41CE-1AE7-403A-B178-D69E8DAFB08B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "openHAB.Windows", "src\openHAB.Windows\openHAB.Windows.csproj", "{D65E41CE-1AE7-403A-B178-D69E8DAFB08B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "openHAB.Core.Client", "src\openHAB.Core.Client\openHAB.Core.Client.csproj", "{3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "openHAB.Common", "src\openHAB.Common\openHAB.Common.csproj", "{E53B599D-4BDB-40B1-86E4-47FB90420138}" +EndProject +Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "openHAB.Windows.Package", "src\openHAB.Windows.Package\openHAB.Windows.Package.wapproj", "{4E463E63-604D-4C3D-8A13-273B787C99B2}" +EndProject +Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "openHAB.Windows.Package.Beta", "src\openHAB.Windows.Package.Beta\openHAB.Windows.Package.Beta.wapproj", "{DC801784-7226-423E-AB0D-C34DFC8AB938}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM = Debug|ARM + Debug|arm64 = Debug|arm64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 - Release|ARM = Release|ARM + Release|arm64 = Release|arm64 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Debug|ARM.ActiveCfg = Debug|ARM - {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Debug|ARM.Build.0 = Debug|ARM + {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Debug|arm64.ActiveCfg = Debug|arm64 + {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Debug|arm64.Build.0 = Debug|arm64 {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Debug|x64.ActiveCfg = Debug|x64 {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Debug|x64.Build.0 = Debug|x64 {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Debug|x86.ActiveCfg = Debug|x86 {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Debug|x86.Build.0 = Debug|x86 - {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Release|ARM.ActiveCfg = Release|ARM - {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Release|ARM.Build.0 = Release|ARM + {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Release|arm64.ActiveCfg = Release|arm64 + {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Release|arm64.Build.0 = Release|arm64 {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Release|x64.ActiveCfg = Release|x64 {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Release|x64.Build.0 = Release|x64 {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Release|x86.ActiveCfg = Release|x86 {2A8FD10E-4EE5-4E25-B47C-2296E071F482}.Release|x86.Build.0 = Release|x86 - {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|ARM.ActiveCfg = Debug|ARM - {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|ARM.Build.0 = Debug|ARM - {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|ARM.Deploy.0 = Debug|ARM + {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|arm64.ActiveCfg = Debug|arm64 + {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|arm64.Build.0 = Debug|arm64 + {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|arm64.Deploy.0 = Debug|arm64 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|x64.ActiveCfg = Debug|x64 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|x64.Build.0 = Debug|x64 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|x64.Deploy.0 = Debug|x64 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|x86.ActiveCfg = Debug|x86 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|x86.Build.0 = Debug|x86 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Debug|x86.Deploy.0 = Debug|x86 - {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|ARM.ActiveCfg = Release|ARM - {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|ARM.Build.0 = Release|ARM - {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|ARM.Deploy.0 = Release|ARM + {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|arm64.ActiveCfg = Release|arm64 + {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|arm64.Build.0 = Release|arm64 + {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|arm64.Deploy.0 = Release|arm64 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|x64.ActiveCfg = Release|x64 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|x64.Build.0 = Release|x64 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|x64.Deploy.0 = Release|x64 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|x86.ActiveCfg = Release|x86 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|x86.Build.0 = Release|x86 {D65E41CE-1AE7-403A-B178-D69E8DAFB08B}.Release|x86.Deploy.0 = Release|x86 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Debug|arm64.ActiveCfg = Debug|arm64 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Debug|arm64.Build.0 = Debug|arm64 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Debug|x64.ActiveCfg = Debug|x64 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Debug|x64.Build.0 = Debug|x64 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Debug|x86.ActiveCfg = Debug|x86 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Debug|x86.Build.0 = Debug|x86 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Release|arm64.ActiveCfg = Release|arm64 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Release|arm64.Build.0 = Release|arm64 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Release|x64.ActiveCfg = Release|x64 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Release|x64.Build.0 = Release|x64 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Release|x86.ActiveCfg = Release|x86 + {3B8F31DD-1AF2-47CF-B7DB-3B732FE4EC5A}.Release|x86.Build.0 = Release|x86 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Debug|arm64.ActiveCfg = Debug|arm64 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Debug|arm64.Build.0 = Debug|arm64 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Debug|x64.ActiveCfg = Debug|x64 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Debug|x64.Build.0 = Debug|x64 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Debug|x86.ActiveCfg = Debug|x86 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Debug|x86.Build.0 = Debug|x86 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Release|arm64.ActiveCfg = Release|arm64 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Release|arm64.Build.0 = Release|arm64 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Release|x64.ActiveCfg = Release|x64 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Release|x64.Build.0 = Release|x64 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Release|x86.ActiveCfg = Release|x86 + {E53B599D-4BDB-40B1-86E4-47FB90420138}.Release|x86.Build.0 = Release|x86 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Debug|arm64.ActiveCfg = Debug|arm64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Debug|arm64.Build.0 = Debug|arm64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Debug|arm64.Deploy.0 = Debug|arm64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Debug|x64.ActiveCfg = Debug|x64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Debug|x64.Build.0 = Debug|x64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Debug|x64.Deploy.0 = Debug|x64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Debug|x86.ActiveCfg = Debug|x86 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Debug|x86.Build.0 = Debug|x86 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Debug|x86.Deploy.0 = Debug|x86 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Release|arm64.ActiveCfg = Release|arm64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Release|arm64.Build.0 = Release|arm64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Release|arm64.Deploy.0 = Release|arm64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Release|x64.ActiveCfg = Release|x64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Release|x64.Build.0 = Release|x64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Release|x64.Deploy.0 = Release|x64 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Release|x86.ActiveCfg = Release|x86 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Release|x86.Build.0 = Release|x86 + {4E463E63-604D-4C3D-8A13-273B787C99B2}.Release|x86.Deploy.0 = Release|x86 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Debug|arm64.ActiveCfg = Debug|ARM64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Debug|arm64.Build.0 = Debug|ARM64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Debug|arm64.Deploy.0 = Debug|ARM64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Debug|x64.ActiveCfg = Debug|x64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Debug|x64.Build.0 = Debug|x64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Debug|x64.Deploy.0 = Debug|x64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Debug|x86.ActiveCfg = Debug|x86 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Debug|x86.Build.0 = Debug|x86 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Debug|x86.Deploy.0 = Debug|x86 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Release|arm64.ActiveCfg = Release|ARM64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Release|arm64.Build.0 = Release|ARM64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Release|arm64.Deploy.0 = Release|ARM64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Release|x64.ActiveCfg = Release|x64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Release|x64.Build.0 = Release|x64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Release|x64.Deploy.0 = Release|x64 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Release|x86.ActiveCfg = Release|x86 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Release|x86.Build.0 = Release|x86 + {DC801784-7226-423E-AB0D-C34DFC8AB938}.Release|x86.Deploy.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index ba1ec8a1..ee721452 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,59 @@ ## Introduction -openHAB Windows application is a native client for openHAB 2 & 3. It uses REST API of openHAB to render -sitemaps of your openHAB. +The openHAB Windows application is a native client for openHAB 3 & 4, leveraging the REST API to render sitemaps and provide a seamless interface for managing your smart home devices. + +openHAB (open Home Automation Bus) is an open-source, technology-agnostic home automation platform that integrates various smart home systems and technologies into one solution. It is designed to be flexible and scalable, allowing users to automate their homes with ease. openHAB supports a wide range of devices and protocols, making it a versatile choice for home automation enthusiasts. + +### Key Features +- **Extensibility**: With a modular architecture, openHAB can be extended with add-ons to support new devices and technologies. +- **Community-Driven**: Maintained by a vibrant community of developers and users who contribute to its continuous improvement. +- **Cross-Platform**: Runs on various operating systems, including Windows, macOS, Linux, and Raspberry Pi. +- **User-Friendly Interfaces**: Offers multiple user interfaces, such as web-based UIs, mobile apps, and voice control, to manage and monitor your smart home. + +For more information, visit the [official openHAB website](https://www.openhab.org/). ## Builds +[![openHAB Build and Release app](https://github.com/openhab/openhab-windows/actions/workflows/openhab.yml/badge.svg)](https://github.com/openhab/openhab-windows/actions/workflows/openhab.yml) + +## Microsoft App Store + +### Beta App +The beta app offers early access to upcoming features and fixes, allowing users to test and provide feedback before these updates are officially released in the main application. This helps ensure a stable and polished experience for all users. + +[![Link to Windows Store](/docs/get_microsoft_store.png)](https://www.microsoft.com/store/apps/9N140MD0NJ6T) + +### Production App +The production app is the stable version of the openHAB Windows application, providing a reliable and tested experience for managing your smart home devices. It includes all the latest features and improvements that have been thoroughly vetted by the community and developers. -[![CI Build](https://github.com/openhab/openhab-windows/actions/workflows/ci.yml/badge.svg)](https://github.com/openhab/openhab-windows/actions/workflows/ci.yml) -[![Latest App Release Build](https://github.com/openhab/openhab-windows/actions/workflows/app-release.yml/badge.svg)](https://github.com/openhab/openhab-windows/actions/workflows/app-release.yml) +[![Link to Windows Store](/docs/get_microsoft_store.png)](https://www.microsoft.com/store/apps/9NMQ39CTWXGT) ## Code Analysis -The app project is using SonarQube hosted by SonarCloud to analyse the code for issues and code quality. +The project utilizes SonarQube, hosted by SonarCloud, to analyze the code for potential issues and ensure high code quality. [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-white.svg)](https://sonarcloud.io/dashboard?id=openhab_openhab-windows) ### Quality Status -| Branch | Quality Gate Status | Bugs |Code Smells -|--------|---------------------|------ |------------| -| beta |||| -| main | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=openhab_openhab-windows&metric=alert_status)](https://sonarcloud.io/dashboard?id=openhab_openhab-windows)| [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=openhab_openhab-windows&metric=bugs)](https://sonarcloud.io/dashboard?id=openhab_openhab-windows)|[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=openhab_openhab-windows&metric=code_smells)](https://sonarcloud.io/dashboard?id=openhab_openhab-windows)| +| Branch | Quality Gate Status | Bugs | Code Smells | +|--------|---------------------|------|-------------| +| beta | | | | +| main | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=openhab_openhab-windows&metric=alert_status)](https://sonarcloud.io/dashboard?id=openhab_openhab-windows) | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=openhab_openhab-windows&metric=bugs)](https://sonarcloud.io/dashboard?id=openhab_openhab-windows) | [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=openhab_openhab-windows&metric=code_smells)](https://sonarcloud.io/dashboard?id=openhab_openhab-windows) | -## Setting up development environment +## Setting up the Development Environment -If you want to contribute to the Windows application we are here to help you to set up -development environment. openHAB Windows app is developed using Visual Studio 2019 and later. +If you want to contribute to the Windows application, we are here to help you set up your development environment. The openHAB Windows app is developed using Visual Studio 2019 and later versions. -- Download and install [Visual Studio Community Edition](https://www.visualstudio.com/downloads/) -- During install, make sure to select UWP SDK 17763 and SDK 19041 -- Check out the latest code from github -- Open the project in Visual Studio (File -> Open, Project/Solution) -- Rebuild the solution to fetch all missing NuGet packages +### Steps to Set Up: +1. Download and install [Visual Studio Community Edition](https://www.visualstudio.com/downloads/). +2. During installation, ensure to select Windows 10 SDK 10.0.20348.0 and Windows 11 SDK 10.0.26100.0. +3. Check out the latest code from GitHub. +4. Install pre-commit hook tooling by executing `setup_pre-commit.ps1` +4. Open the project in Visual Studio (File -> Open -> Project/Solution). +5. Rebuild the solution to fetch all missing NuGet packages. -You are ready to contribute! +You are now ready to contribute! -Before producing any amount of code please have a look at [contribution guidelines](https://github.com/openhab/openhab.windows/blob/master/CONTRIBUTING.md) +Before writing any code, please review our [contribution guidelines](https://github.com/openhab/openhab.windows/blob/master/CONTRIBUTING.md). ## Trademark Disclaimer -Product names, logos, brands and other trademarks referred to within the openHAB website are the -property of their respective trademark holders. These trademark holders are not affiliated with -openHAB or our website. They do not sponsor or endorse our materials. +Product names, logos, brands, and other trademarks referred to within the openHAB website are the property of their respective trademark holders. These trademark holders are not affiliated with openHAB or our website. They do not sponsor or endorse our materials. diff --git a/docs/get_microsoft_store.png b/docs/get_microsoft_store.png new file mode 100644 index 00000000..a5f827bf Binary files /dev/null and b/docs/get_microsoft_store.png differ diff --git a/docs/screenshots/screenshot1.png b/docs/screenshots/screenshot1.png deleted file mode 100644 index 24144484..00000000 Binary files a/docs/screenshots/screenshot1.png and /dev/null differ diff --git a/docs/screenshots/screenshot2.png b/docs/screenshots/screenshot2.png deleted file mode 100644 index 3bc8a1a3..00000000 Binary files a/docs/screenshots/screenshot2.png and /dev/null differ diff --git a/docs/screenshots/screenshot3.png b/docs/screenshots/screenshot3.png deleted file mode 100644 index 0b7cf09d..00000000 Binary files a/docs/screenshots/screenshot3.png and /dev/null differ diff --git a/docs/screenshots/screenshot4.png b/docs/screenshots/screenshot4.png deleted file mode 100644 index 33de7dee..00000000 Binary files a/docs/screenshots/screenshot4.png and /dev/null differ diff --git a/setup_pre-commit.ps1 b/setup_pre-commit.ps1 new file mode 100644 index 00000000..afd9ee63 --- /dev/null +++ b/setup_pre-commit.ps1 @@ -0,0 +1,18 @@ +# setup_pre-commit.ps +# This script initializes pre-commit hooks for the repository + +# Ensure pre-commit is installed +if (-not (Get-Command pre-commit -ErrorAction SilentlyContinue)) { + Write-Host "pre-commit is not installed. Installing..." + pip install pre-commit +} + +# Navigate to the repository root +$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location $repoRoot + +# Install pre-commit hooks +Write-Host "Installing pre-commit hooks..." +pre-commit install --hook-type pre-commit --hook-type pre-push --hook-type commit-msg + +Write-Host "Pre-commit hooks installed successfully." diff --git a/src/openHAB.Common/AppResources.cs b/src/openHAB.Common/AppResources.cs new file mode 100644 index 00000000..6ba1874e --- /dev/null +++ b/src/openHAB.Common/AppResources.cs @@ -0,0 +1,44 @@ +using Microsoft.Windows.ApplicationModel.Resources; + +namespace openHAB.Common; + +/// +/// Resources. +/// +public static class AppResources +{ + private static ResourceLoader _resourceLoader; + private static ResourceLoader _errorResourceLoader; + + /// + /// Gets the localized UI values. + /// + public static ResourceLoader Values + { + get + { + if (_resourceLoader == null) + { + _resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath()); + } + + return _resourceLoader; + } + } + + /// + /// Gets the localized error strings. + /// + public static ResourceLoader Errors + { + get + { + if (_errorResourceLoader == null) + { + _errorResourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath()); + } + + return _errorResourceLoader; + } + } +} diff --git a/src/openHAB.Common/openHAB.Common.csproj b/src/openHAB.Common/openHAB.Common.csproj new file mode 100644 index 00000000..1abdd938 --- /dev/null +++ b/src/openHAB.Common/openHAB.Common.csproj @@ -0,0 +1,15 @@ + + + net8.0-windows10.0.26100.0 + 10.0.17763.0 + openHAB.Common + win-x86;win-x64;win-arm64 + true + 10.0.20348.0 + x86;x64;arm64 + + + + + + diff --git a/src/openHAB.Core.Client/AutoNumberToStringConverter.cs b/src/openHAB.Core.Client/AutoNumberToStringConverter.cs new file mode 100644 index 00000000..c7b05ac8 --- /dev/null +++ b/src/openHAB.Core.Client/AutoNumberToStringConverter.cs @@ -0,0 +1,38 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace openHAB.Core.Client; + +public class AutoNumberToStringConverter : JsonConverter +{ + public override bool CanConvert(Type typeToConvert) + { + return typeof(string) == typeToConvert; + } + + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Number) + { + return reader.TryGetInt64(out long l) ? + l.ToString() : + reader.GetDouble().ToString(); + } + + if (reader.TokenType == JsonTokenType.String) + { + return reader.GetString(); + } + + using (JsonDocument document = JsonDocument.ParseValue(ref reader)) + { + return document.RootElement.Clone().ToString(); + } + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/src/openHAB.Core.Client/Common/HttpResponseResult.cs b/src/openHAB.Core.Client/Common/HttpResponseResult.cs new file mode 100644 index 00000000..2c2eca0c --- /dev/null +++ b/src/openHAB.Core.Client/Common/HttpResponseResult.cs @@ -0,0 +1,55 @@ +using System; +using System.Net; + +namespace openHAB.Core.Client.Common; + +/// HTTP response result. +/// Content Type. +public class HttpResponseResult +{ + /// Initializes a new instance of the class. + /// The content. + /// The status code. + public HttpResponseResult(T content, HttpStatusCode? statusCode) + : this(content, statusCode, null) + { + } + + /// Initializes a new instance of the class. + /// The content. + /// The status code. + /// The exception. + public HttpResponseResult(T content, HttpStatusCode? statusCode, Exception exception) + { + Content = content; + StatusCode = statusCode; + Exception = exception; + } + + /// Gets the deserailized HTTP response content. + /// Deserailized HTTP response content. + public T Content + { + get; + private set; + } + + /// + /// Gets the exception when available. + /// + /// + /// The exception. + /// + public Exception Exception + { + get; + private set; + } + + /// Gets the http status code. + /// The status code. + public HttpStatusCode? StatusCode + { + get; private set; + } +} diff --git a/src/openHAB.Core.Client/Connection/ConnectionService.cs b/src/openHAB.Core.Client/Connection/ConnectionService.cs new file mode 100644 index 00000000..651de9c5 --- /dev/null +++ b/src/openHAB.Core.Client/Connection/ConnectionService.cs @@ -0,0 +1,175 @@ +using System; +using System.Net.Http; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.WinUI.Helpers; +using Microsoft.Extensions.Logging; +using openHAB.Common; +using openHAB.Core.Client.Common; +using openHAB.Core.Client.Connection.Contracts; +using openHAB.Core.Client.Connection.Models; +using openHAB.Core.Client.Messages; +using openHAB.Core.Client.Models; + +namespace openHAB.Core.Client.Connection; + + +/// +/// +public class ConnectionService : IConnectionService +{ + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private Models.Connection _connection; + + /// + /// Initializes a new instance of the class. + /// + /// The OpenHAB HTTP client. + /// The logger. + public ConnectionService(IHttpClientFactory httpClientFactory, ILogger logger) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + } + + /// + public Models.Connection CurrentConnection => _connection; + + /// + public async Task> CheckUrlReachability(Models.Connection connection) + { + if (string.IsNullOrWhiteSpace(connection?.Url)) + { + return new HttpResponseResult(false, null); + } + + if (!connection.Url.EndsWith("/", StringComparison.InvariantCultureIgnoreCase)) + { + connection.Url += "/"; + } + + HttpResponseResult result = await GetOpenHABServerInfo(connection).ConfigureAwait(false); + return result.Content == null + ? new HttpResponseResult(false, null, result.Exception) + : new HttpResponseResult(true, result.StatusCode); + } + + /// + public async Task DetectAndRetrieveConnection(Models.Connection localConnection, Models.Connection remoteConnection, bool isRunningInDemoMode) + { + _logger.LogInformation("Validate Connection"); + _logger.LogInformation("APP is running in demo mode: {IsRunningInDemoMode}", isRunningInDemoMode); + + if (string.IsNullOrWhiteSpace(localConnection?.Url) && + string.IsNullOrWhiteSpace(remoteConnection?.Url) && + !isRunningInDemoMode) + { + return null; + } + + if (isRunningInDemoMode) + { + _connection = new DemoConnectionProfile().CreateConnection(); + return _connection; + } + + bool meteredConnection = NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection; + _logger.LogInformation("Metered Connection Type: {MeteredConnection}", meteredConnection); + + if (meteredConnection) + { + if (string.IsNullOrEmpty(remoteConnection?.Url)) + { + _logger.LogWarning("No remote url configured"); + return null; + } + + _connection = remoteConnection; + return _connection; + } + + HttpResponseResult result = await CheckUrlReachability(localConnection).ConfigureAwait(false); + _logger.LogInformation("OpenHab server is reachable: {IsReachable}", result.Content); + + if (result.Content) + { + _connection = localConnection; + return _connection; + } + + if (string.IsNullOrWhiteSpace(remoteConnection?.Url)) + { + StrongReferenceMessenger.Default.Send(new ConnectionErrorMessage(AppResources.Errors.GetString("ConnectionTestFailed"))); + _logger.LogWarning("OpenHab server url is not valid"); + return null; + } + + result = await CheckUrlReachability(remoteConnection).ConfigureAwait(false); + if (!result.Content) + { + StrongReferenceMessenger.Default.Send(new ConnectionErrorMessage(AppResources.Errors.GetString("ConnectionTestFailed"))); + _logger.LogWarning("OpenHab server url is not valid"); + return null; + } + + _connection = remoteConnection; + return _connection; + } + + /// + public async Task> GetOpenHABServerInfo(Models.Connection connection) + { + try + { + HttpClient httpClient = connection.Type switch + { + HttpClientType.Local => _httpClientFactory.CreateClient("local"), + HttpClientType.Remote => _httpClientFactory.CreateClient("remote"), + _ => throw new OpenHABException("Invalid connection type") + }; + + HttpResponseMessage result = await httpClient.GetAsync(Constants.API.ServerInformation).ConfigureAwait(false); + if (!result.IsSuccessStatusCode) + { + _logger.LogError("HTTP request get OpenHab version failed, ErrorCode:'{StatusCode}'", result.StatusCode); + throw new OpenHABException($"{result.StatusCode} received from server"); + } + + string responseBody = await result.Content.ReadAsStringAsync(); + APIInfo apiInfo = JsonSerializer.Deserialize(responseBody); + + if (int.TryParse(apiInfo.Version, out int apiVersion) && apiVersion < 4) + { + return new HttpResponseResult(new ServerInfo { Version = OpenHABVersion.Two }, result.StatusCode); + } + + string runtimeversion = Regex.Replace(apiInfo?.RuntimeInfo.Version, "[^.0-9]", string.Empty, RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1)); + if (!Version.TryParse(runtimeversion, out Version serverVersion)) + { + string message = "Not able to parse runtime version from openHAB server"; + _logger.LogError(message); + throw new OpenHABException(message); + } + + return new HttpResponseResult(new ServerInfo + { + Version = (OpenHABVersion)serverVersion.Major, + RuntimeVersion = apiInfo?.RuntimeInfo.Version, + Build = apiInfo.RuntimeInfo.BuildString + }, result.StatusCode); + } + catch (Exception ex) when (ex is ArgumentNullException or InvalidOperationException or HttpRequestException) + { + _logger.LogError(ex, "GetOpenHABServerInfo failed"); + return new HttpResponseResult(null, null, ex); + } + catch (Exception ex) + { + _logger.LogError(ex, "GetOpenHABServerInfo failed."); + return new HttpResponseResult(null, null, ex); + } + } +} diff --git a/src/openHAB.Core.Client/Connection/Contracts/IConnectionProfile.cs b/src/openHAB.Core.Client/Connection/Contracts/IConnectionProfile.cs new file mode 100644 index 00000000..9d663780 --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Contracts/IConnectionProfile.cs @@ -0,0 +1,70 @@ +using openHAB.Core.Client.Connection.Models; + +namespace openHAB.Core.Client.Connection.Contracts; + +/// +/// Interface for connection profiles. +/// +public interface IConnectionProfile +{ + /// Gets a value indicating whether [host URL] value can be modified. + /// + /// true if [host URL configuration] can be modified; otherwise, false. + bool AllowHostUrlConfiguration + { + get; + } + + /// Gets a value indicating whether [allow ignore SSL certificate] issues option is available. + /// + /// true if [allow ignore SSL certificate] is available; otherwise, false. + bool AllowIgnoreSSLCertificate + { + get; + } + + /// Gets a value indicating whether [allow ignore SSL hostname] issue option is available. + /// + /// true. if [allow ignore SSL hostname] can be enabled; otherwise, false. + bool AllowIgnoreSSLHostname + { + get; + } + + /// Gets the connection profile id. + /// The id. + int Id + { + get; + } + + /// Gets the connection profile name. + /// The profile name. + string Name + { + get; + } + + /// Gets the profile type. + /// The type. + HttpClientType Type + { + get; + } + /// Gets the main UI URL. + /// The main UI URL. + string MainUIUrl + { + get; + } + /// Gets the URL. + /// The URL. + string Url + { + get; + } + + /// Creates the connection based on the profile. + /// Preconfigured connection. + Models.Connection CreateConnection(); +} diff --git a/src/openHAB.Core.Client/Connection/Contracts/IConnectionService.cs b/src/openHAB.Core.Client/Connection/Contracts/IConnectionService.cs new file mode 100644 index 00000000..a9cc451d --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Contracts/IConnectionService.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using openHAB.Core.Client.Common; +using openHAB.Core.Client.Models; + +namespace openHAB.Core.Client.Connection.Contracts; + +/// +/// Service for connection management. +/// +public interface IConnectionService +{ + /// + /// Gets or sets the current connection to openHAB instance. + /// + Models.Connection CurrentConnection + { + get; + } + + /// Checks the URL reachability. + /// Defines settings for local or remote connections. + /// >A representing the asynchronous operation. + Task> CheckUrlReachability(Models.Connection connection); + + /// Detects if connection is local or remote and provides the connection information. + /// The app settings. + /// Return the connection information for local/remote. + Task DetectAndRetrieveConnection(Models.Connection localConnection, Models.Connection remoteConnection, bool isRunningInDemoMode); + + /// + /// Gets information about the openHAB server. + /// + /// Connection information. + /// Server information about openHAB instance. + Task> GetOpenHABServerInfo(Models.Connection connection); +} diff --git a/src/openHAB.Core.Client/Connection/Models/CloudConnectionProfile.cs b/src/openHAB.Core.Client/Connection/Models/CloudConnectionProfile.cs new file mode 100644 index 00000000..af46bb30 --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/CloudConnectionProfile.cs @@ -0,0 +1,62 @@ +using openHAB.Common; +using openHAB.Core.Client.Connection.Contracts; + +namespace openHAB.Core.Client.Connection.Models; + +/// Connection profile for for local custom connection to OpenHab server. +/// +public class CloudConnectionProfile : IConnectionProfile +{ + /// + public bool AllowHostUrlConfiguration + { + get => false; + } + + /// + public bool AllowIgnoreSSLCertificate + { + get => false; + } + + /// + public bool AllowIgnoreSSLHostname + { + get => false; + } + + /// + public int Id + { + get => 4; + } + + /// + public string Name + { + get => AppResources.Values.GetString("RemoteDefaultConnection"); + } + + /// + public HttpClientType Type + { + get => HttpClientType.Remote; + } + + public string MainUIUrl + { + get => "https://home.myopenhab.org/"; + } + + public string Url => "https://myopenhab.org/"; + + /// + public Connection CreateConnection() + { + return new Connection() + { + Type = HttpClientType.Remote, + Url = Url + }; + } +} diff --git a/src/openHAB.Core.Client/Connection/Models/Connection.cs b/src/openHAB.Core.Client/Connection/Models/Connection.cs new file mode 100644 index 00000000..75bb0dde --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/Connection.cs @@ -0,0 +1,120 @@ +using System.Text.Json.Serialization; +using openHAB.Core.Client.Connection.Contracts; + +namespace openHAB.Core.Client.Connection.Models; + + +/// +/// Connection configuration for OpenHAB service or cloud instance. +/// +public class Connection +{ + /// Gets or sets the connection profile. + /// The profile. + [JsonIgnore] + public IConnectionProfile Profile + { + get; + set; + } + + /// + /// Gets or sets the connection profile identifier. + /// + /// The identifier of the connection profile. + public int ProfileId + { + get + { + return this.Profile.Id; + } + set + { + Profile = ConnectionProfiles.GetProfile(value); + } + } + + /// Gets or sets the type of the connection. + /// The type of the connection. + public HttpClientType Type + { + get; + set; + } + + /// + /// Gets or sets the URL to the OpenHAB server. + /// + public string Url + { + get; + set; + } + + /// + /// Gets or sets the username for the OpenHAB server connection. + /// + public string Username + { + get; + set; + } + + /// + /// Gets or sets the password for the OpenHAB connection. + /// + public string Password + { + get; + set; + } + + /// + /// Gets or sets a value indicating whether the application will ignore the SSL certificate. + /// + public bool? WillIgnoreSSLCertificate + { + get; + set; + } + + /// + /// Gets or sets a value indicating whether the application will ignore the SSL hostname. + /// + public bool? WillIgnoreSSLHostname + { + get; + set; + } + + /// + /// Gets or sets for openHAB MainUI URL. + /// + [JsonIgnore] + public string MainUIUrl + { + get + { + if (Profile is LocalConnectionProfile) + { + return Url; + } + else if (Profile is RemoteConnectionProfile) + { + return Url; + } + else if (Profile is CloudConnectionProfile) + { + return Profile.MainUIUrl; + } + else if (Profile is DefaultConnectionProfile) + { + return Profile.MainUIUrl; + } + else + { + return Url; + } + } + } +} diff --git a/src/openHAB.Core.Client/Connection/Models/ConnectionProfile.cs b/src/openHAB.Core.Client/Connection/Models/ConnectionProfile.cs new file mode 100644 index 00000000..0416778e --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/ConnectionProfile.cs @@ -0,0 +1,28 @@ +using System; + +namespace openHAB.Core.Client.Connection.Models; + +/// Specifies the current connection profile. +public class ConnectionProfile +{ + /// Gets or sets the connection name. + /// The name. + public string Name + { + get; set; + } + + /// Gets or sets the URL. + /// The URL. + public Uri Url + { + get; set; + } + + /// Gets or sets the type of the connection. + /// The type of the connection. + public ConnectionType ConnectionType + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Connection/Models/ConnectionProfiles.cs b/src/openHAB.Core.Client/Connection/Models/ConnectionProfiles.cs new file mode 100644 index 00000000..295afa70 --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/ConnectionProfiles.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using openHAB.Core.Client.Connection.Contracts; + +namespace openHAB.Core.Client.Connection.Models; + +/// +/// Manages connection profiles. +/// +public class ConnectionProfiles +{ + private static readonly Dictionary _profiles = new Dictionary() + { + { 1, new DefaultConnectionProfile() }, + { 2, new LocalConnectionProfile() }, + { 3, new RemoteConnectionProfile() }, + { 4, new CloudConnectionProfile() } + }; + + /// + /// Gets the connection profile by the specified identifier. + /// + /// The identifier of the connection profile. + /// The connection profile associated with the specified identifier. + public static IConnectionProfile GetProfile(int id) + { + return _profiles[id]; + } + + /// + /// Gets all connection profiles. + /// + /// A list of all connection profiles. + public static List GetProfiles() + { + return new List(_profiles.Values); + } +} diff --git a/src/openHAB.Core.Client/Connection/Models/ConnectionState.cs b/src/openHAB.Core.Client/Connection/Models/ConnectionState.cs new file mode 100644 index 00000000..34c80453 --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/ConnectionState.cs @@ -0,0 +1,22 @@ +namespace openHAB.Core.Client.Connection.Models; + +/// +/// Reflects the State for an connection. +/// +public enum ConnectionState +{ + /// + /// OpenHAB instance is reachable via url + /// + OK, + + /// + /// OpenHAB instance can not reach via check url + /// + Failed, + + /// + /// State is unknown + /// + Unknown, +} diff --git a/src/openHAB.Core.Client/Connection/Models/ConnectionType.cs b/src/openHAB.Core.Client/Connection/Models/ConnectionType.cs new file mode 100644 index 00000000..b8c051f2 --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/ConnectionType.cs @@ -0,0 +1,11 @@ +namespace openHAB.Core.Client.Connection.Models; + +/// Defines connection types for OpenHAB instance. +public enum ConnectionType +{ + /// Local connection to OpenHAB instance. + Local = 0, + + /// Remote connection to OpenHAB instance. + Remote = 1, +} diff --git a/src/openHAB.Core.Client/Connection/Models/DefaultConnectionProfile.cs b/src/openHAB.Core.Client/Connection/Models/DefaultConnectionProfile.cs new file mode 100644 index 00000000..8f2af68a --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/DefaultConnectionProfile.cs @@ -0,0 +1,63 @@ +using openHAB.Common; +using openHAB.Core.Client.Connection.Contracts; + +namespace openHAB.Core.Client.Connection.Models; + +/// Connection profile for local default connection to OpenHab server. +/// +public class DefaultConnectionProfile : IConnectionProfile +{ + /// + public bool AllowHostUrlConfiguration + { + get => false; + } + + /// + public bool AllowIgnoreSSLCertificate + { + get => true; + } + + /// + public bool AllowIgnoreSSLHostname + { + get => true; + } + + /// + public int Id + { + get => 1; + } + + /// + public string Name + { + get => AppResources.Values.GetString("LocalDefaultConnection"); + } + + /// + public HttpClientType Type + { + get => HttpClientType.Local; + } + + /// + public string Url => "https://openhab:8443"; + + /// + public string MainUIUrl => Url; + + /// + public Connection CreateConnection() + { + return new Connection() + { + Url = Url, + Type = Type, + WillIgnoreSSLCertificate = true, + WillIgnoreSSLHostname = true, + }; + } +} diff --git a/src/openHAB.Core.Client/Connection/Models/DemoConnectionProfile.cs b/src/openHAB.Core.Client/Connection/Models/DemoConnectionProfile.cs new file mode 100644 index 00000000..40aee30b --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/DemoConnectionProfile.cs @@ -0,0 +1,63 @@ +using openHAB.Common; +using openHAB.Core.Client.Connection.Contracts; + +namespace openHAB.Core.Client.Connection.Models; + +/// Connection profile for for local custom connection to OpenHab server. +/// +public class DemoConnectionProfile : IConnectionProfile +{ + /// + public bool AllowHostUrlConfiguration + { + get => false; + } + + /// + public bool AllowIgnoreSSLCertificate + { + get => false; + } + + /// + public bool AllowIgnoreSSLHostname + { + get => false; + } + + /// + public int Id + { + get => 4; + } + + /// + public string Name + { + get => AppResources.Values.GetString("DemoConnection"); + } + + /// + public HttpClientType Type + { + get => HttpClientType.Remote; + } + + /// + public string MainUIUrl => Url; + + /// + public string Url => Constants.API.DemoModeUrl; + + /// + public Connection CreateConnection() + { + return new Connection() + { + Type = HttpClientType.Remote, + Url = Url, + Username = Constants.API.DemoModeUser, + Password = Constants.API.DemoModeUserPwd + }; + } +} diff --git a/src/openHAB.Core.Client/Connection/Models/HttpClientType.cs b/src/openHAB.Core.Client/Connection/Models/HttpClientType.cs new file mode 100644 index 00000000..330d889b --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/HttpClientType.cs @@ -0,0 +1,17 @@ +namespace openHAB.Core.Client.Connection.Models; + +/// +/// Defines the connection type to a local or remote openHAB instance. +/// +public enum HttpClientType +{ + /// + /// Local openHab http connection. + /// + Local, + + /// + /// Remote openHab http connection. + /// + Remote, +} diff --git a/src/openHAB.Core.Client/Connection/Models/LocalConnectionProfile.cs b/src/openHAB.Core.Client/Connection/Models/LocalConnectionProfile.cs new file mode 100644 index 00000000..27374c58 --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/LocalConnectionProfile.cs @@ -0,0 +1,59 @@ +using openHAB.Common; +using openHAB.Core.Client.Connection.Contracts; + +namespace openHAB.Core.Client.Connection.Models; + +/// Connection profile for local custom connection to OpenHab server. +/// +public class LocalConnectionProfile : IConnectionProfile +{ + /// + public bool AllowHostUrlConfiguration + { + get => true; + } + + /// + public bool AllowIgnoreSSLCertificate + { + get => true; + } + + /// + public bool AllowIgnoreSSLHostname + { + get => true; + } + + /// Gets the connection profile îd. + /// The îd. + public int Id + { + get => 2; + } + + /// + public string Name + { + get => AppResources.Values.GetString("LocalCustomConnection"); + } + + /// + public HttpClientType Type + { + get => HttpClientType.Local; + } + + public string MainUIUrl => null; + + public string Url => null; + + /// + public Connection CreateConnection() + { + return new Connection() + { + Type = HttpClientType.Local, + }; + } +} diff --git a/src/openHAB.Core.Client/Connection/Models/RemoteConnectionProfile.cs b/src/openHAB.Core.Client/Connection/Models/RemoteConnectionProfile.cs new file mode 100644 index 00000000..aab9b469 --- /dev/null +++ b/src/openHAB.Core.Client/Connection/Models/RemoteConnectionProfile.cs @@ -0,0 +1,60 @@ +using openHAB.Common; +using openHAB.Core.Client.Connection.Contracts; + +namespace openHAB.Core.Client.Connection.Models; + +/// Connection profile for for local custom connection to OpenHab server. +/// +public class RemoteConnectionProfile : IConnectionProfile +{ + /// + public bool AllowHostUrlConfiguration + { + get => true; + } + + /// + public bool AllowIgnoreSSLCertificate + { + get => true; + } + + /// + public bool AllowIgnoreSSLHostname + { + get => true; + } + + /// + public int Id + { + get => 3; + } + + /// + public string Name + { + get => AppResources.Values.GetString("RemoteCustomConnection"); + } + + /// + public HttpClientType Type + { + get => HttpClientType.Remote; + } + + /// + public string MainUIUrl => "https://home.myopenhab.org/"; + + /// + public string Url => null; + + /// + public Connection CreateConnection() + { + return new Connection() + { + Type = HttpClientType.Remote, + }; + } +} diff --git a/src/openHAB.Core.Client/Constants.cs b/src/openHAB.Core.Client/Constants.cs new file mode 100644 index 00000000..30bcef59 --- /dev/null +++ b/src/openHAB.Core.Client/Constants.cs @@ -0,0 +1,48 @@ +namespace openHAB.Core.Client; + +/// +/// A class that holds all the constants for the app. +/// +public sealed class Constants +{ + /// + /// Holds the constants used in server calls. + /// + public sealed class API + { + /// + /// Url used for Demo mode. + /// + public const string DemoModeUrl = "https://demo.openhab.org/"; + + /// + /// Username for demo system. + /// + public const string DemoModeUser = "demo"; + + /// + /// User password for demo system. + /// + public const string DemoModeUserPwd = "demo"; + + /// + /// The call to get information about OpenHAB instance. + /// + public const string ServerInformation = "rest/"; + + /// + /// The call to fetch the sitemaps. + /// + public const string Sitemaps = "rest/sitemaps"; + + /// + /// The call to get an event-stream. + /// + public const string Events = "rest/events"; + + /// + /// The call to get items. + /// + public const string Items = "rest/items"; + } +} diff --git a/src/openHAB.Core.Client/Contracts/IOpenHABClient.cs b/src/openHAB.Core.Client/Contracts/IOpenHABClient.cs new file mode 100644 index 00000000..5b582c80 --- /dev/null +++ b/src/openHAB.Core.Client/Contracts/IOpenHABClient.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using openHAB.Core.Client.Common; +using openHAB.Core.Client.Models; + +namespace openHAB.Core.Client.Contracts; + +/// +/// The main SDK interface to OpenHAB. +/// +public interface IOpenHABClient +{ + /// + /// Gets information about the openHAB server. + /// + /// Server information about openHAB instance. + Task> GetOpenHABServerInfo(); + + /// + /// Gets the openHAB item by name from Server. + /// + /// Name of the item. + /// openHab item object. + Task GetItemByName(string itemName); + + /// + /// Loads all the sitemaps. + /// + /// The version of OpenHAB running on the server. + /// Filters for sitemap list. + /// A list of sitemaps. + Task> LoadSitemaps(OpenHABVersion version, List> filters); + + + /// Loads the items from sitemap. + /// The sitemap link. + /// The openHab server version. + /// Returns loaded sitemap + Task> LoadItemsFromSitemap(string sitemapLink, OpenHABVersion version); + + /// + /// Sends a command to an item. + /// + /// The item. + /// The Command. + /// Operation result if the command was successful or not. + Task> SendCommand(Item item, string command); + + /// + /// Reset the connection to the OpenHAB server after changing the settings in the App. + /// + /// The local connection to reset. + /// The remote connection to reset. + /// Indicates whether the App is running in demo mode. + /// A representing the asynchronous operation. + Task ResetConnection(Connection.Models.Connection localConnection, Connection.Models.Connection remoteConnection, bool? isRunningInDemoMode); + + /// + /// Starts listening to server events. + /// + void StartItemUpdates(System.Threading.CancellationToken token); + Task GetSitemap(string sitemapLink, OpenHABVersion version); +} diff --git a/src/openHAB.Core.Client/Event/Contracts/IOpenHABEventParser.cs b/src/openHAB.Core.Client/Event/Contracts/IOpenHABEventParser.cs new file mode 100644 index 00000000..32310124 --- /dev/null +++ b/src/openHAB.Core.Client/Event/Contracts/IOpenHABEventParser.cs @@ -0,0 +1,12 @@ +namespace openHAB.Core.Client.Event.Contracts; + +/// openHAB event parser. +public interface IOpenHABEventParser +{ + /// Parses the openHAB event message. + /// The message. + /// + /// openHAB event object . + /// + OpenHABEvent Parse(string message); +} diff --git a/src/openHAB.Core.Client/Event/EventStreamData.cs b/src/openHAB.Core.Client/Event/EventStreamData.cs new file mode 100644 index 00000000..e23e8bca --- /dev/null +++ b/src/openHAB.Core.Client/Event/EventStreamData.cs @@ -0,0 +1,31 @@ +namespace openHAB.Core.Client.Event; + +/// +/// Data coming from the eventstream. +/// +public class EventStreamData +{ + /// + /// Gets or sets the topic on which the event was fired. + /// + public string Topic + { + get; set; + } + + /// + /// Gets or sets the payload of the event. + /// + public string Payload + { + get; set; + } + + /// + /// Gets or sets the type of event. + /// + public string Type + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Event/EventStreamPayload.cs b/src/openHAB.Core.Client/Event/EventStreamPayload.cs new file mode 100644 index 00000000..8959aa19 --- /dev/null +++ b/src/openHAB.Core.Client/Event/EventStreamPayload.cs @@ -0,0 +1,39 @@ +namespace openHAB.Core.Client.Event; + +/// +/// The payload coming from the event service. +/// +public class EventStreamPayload +{ + /// + /// Gets or sets the type of event. + /// + public string Type + { + get; set; + } + + /// + /// Gets or sets the value of the event. + /// + public string Value + { + get; set; + } + + /// Gets or sets the old type of the event. + /// The old type. + public string OldType + { + get; + set; + } + + /// Gets or sets the old value of the event. + /// The old value. + public string OldValue + { + get; + set; + } +} diff --git a/src/openHAB.Core.Client/Event/OpenHABEvent.cs b/src/openHAB.Core.Client/Event/OpenHABEvent.cs new file mode 100644 index 00000000..3190e4ef --- /dev/null +++ b/src/openHAB.Core.Client/Event/OpenHABEvent.cs @@ -0,0 +1,61 @@ +namespace openHAB.Core.Client.Event; + +/// OpenHAB event. +public class OpenHABEvent +{ + /// Gets the type of the event. + /// The type of the event. + public OpenHABEventType EventType + { + get; + set; + } + + /// Gets or sets the name of the item. + /// The name of the item. + public string ItemName + { + get; + set; + } + + /// Gets or sets the old type. + /// The old type. + public string OldType + { + get; + set; + } + + /// Gets or sets the old value. + /// The old value. + public string OldValue + { + get; + set; + } + + /// Gets or sets the topic. + /// The topic. + public string Topic + { + get; + set; + } + + /// Gets or sets the value. + /// The value. + public string Value + { + get; + set; + } + + /// Gets or sets the type of the value. + /// The type of the value. + public string ValueType + { + get; + set; + } +} diff --git a/src/openHAB.Core.Client/Event/OpenHABEventType.cs b/src/openHAB.Core.Client/Event/OpenHABEventType.cs new file mode 100644 index 00000000..2e36192b --- /dev/null +++ b/src/openHAB.Core.Client/Event/OpenHABEventType.cs @@ -0,0 +1,26 @@ +namespace openHAB.Core.Client.Event; + +/// openHAB event types. +public enum OpenHABEventType +{ + /// Item state event. + ItemStateEvent = 0, + + /// Thing updated event. + ThingUpdatedEvent = 1, + + /// Rule status information event. + RuleStatusInfoEvent = 2, + + /// Item state predicted event. + ItemStatePredictedEvent = 3, + + /// Group item state changed event. + GroupItemStateChangedEvent = 4, + + /// Item state changed event. + ItemStateChangedEvent = 5, + + /// Unknown event. + Unknown = 6 +} diff --git a/src/openHAB.Core.Client/Extensions/OpenHabHttpClientExtension.cs b/src/openHAB.Core.Client/Extensions/OpenHabHttpClientExtension.cs new file mode 100644 index 00000000..82201cc6 --- /dev/null +++ b/src/openHAB.Core.Client/Extensions/OpenHabHttpClientExtension.cs @@ -0,0 +1,103 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Security; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using openHAB.Core.Client.Connection.Models; +using openHAB.Core.Client.Options; + +namespace openHAB.Core.Client.Extensions; + +/// +/// Provides extension methods for adding OpenHAB HTTP clients to the service collection. +/// +public static class OpenHabHttpClientExtension +{ + /// + /// Adds the OpenHAB HTTP clients to the specified service collection. + /// + /// The service collection to add the HTTP clients to. + public static void AddOpenHabHttpClients(this IServiceCollection services) + { + AddHttpClient(services, "local", options => options.LocalConnection); + AddHttpClient(services, "remote", options => options.RemoteConnection); + } + + private static void AddHttpClient(IServiceCollection services, string name, Func getConnection) + { + services.AddHttpClient(name, (sp, client) => + { + var options = sp.GetRequiredService>().Value; + Connection.Models.Connection connection = CreateConnection(getConnection, options); + + ConfigureHttpClient(client, connection); + }).ConfigurePrimaryHttpMessageHandler(sp => + { + var options = sp.GetRequiredService>().Value; + Connection.Models.Connection connection = CreateConnection(getConnection, options); + return CreateHttpClientHandler(connection); + }); + } + + private static Connection.Models.Connection CreateConnection(Func getConnection, ConnectionOptions options) + { + Connection.Models.Connection connection; + if (options.IsRunningInDemoMode.GetValueOrDefault()) + { + connection = new DemoConnectionProfile().CreateConnection(); + } + else + { + connection = getConnection(options); + } + + return connection; + } + + private static void ConfigureHttpClient(HttpClient client, Connection.Models.Connection connection) + { + client.BaseAddress = new Uri(connection.Url); + + if (!string.IsNullOrEmpty(connection.Username) && !string.IsNullOrEmpty(connection.Password)) + { + string basicCredentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{connection.Username}:{connection.Password}")); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basicCredentials); + } + + //client.DefaultRequestHeaders.Add("Accept", "application/json"); + } + + private static HttpClientHandler CreateHttpClientHandler(Connection.Models.Connection connection) + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => + { + if (errors == SslPolicyErrors.None) + { + return true; + } + + bool result = true; + if (errors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors)) + { + result &= connection.WillIgnoreSSLCertificate.GetValueOrDefault(); + } + + if (errors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) + { + result &= connection.WillIgnoreSSLHostname.GetValueOrDefault(); + } + + if (errors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable)) + { + result = false; + } + + return result; + } + }; + } +} diff --git a/src/openHAB.Core.Client/Messages/ConnectionErrorMessage.cs b/src/openHAB.Core.Client/Messages/ConnectionErrorMessage.cs new file mode 100644 index 00000000..92db2ccd --- /dev/null +++ b/src/openHAB.Core.Client/Messages/ConnectionErrorMessage.cs @@ -0,0 +1,25 @@ +namespace openHAB.Core.Client.Messages; + +/// +/// Triggers a visual error on a connection issue. +/// +public class ConnectionErrorMessage +{ + /// + /// Initializes a new instance of the class. + /// + /// The error message. + public ConnectionErrorMessage(string errorMessage) + { + ErrorMessage = errorMessage; + } + + /// + /// Gets or sets the error text. + /// + /// The text. + public string ErrorMessage + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Messages/ItemStateChangedMessage.cs b/src/openHAB.Core.Client/Messages/ItemStateChangedMessage.cs new file mode 100644 index 00000000..ab2d66fe --- /dev/null +++ b/src/openHAB.Core.Client/Messages/ItemStateChangedMessage.cs @@ -0,0 +1,45 @@ +namespace openHAB.Core.Client.Messages; + +/// +/// A message to trigger an item update coming from a server event. +/// +public class ItemStateChangedMessage +{ + /// Initializes a new instance of the class. + /// Name of the item. + /// The value. + /// The old value. + public ItemStateChangedMessage(string itemName, string value, string oldValue) + { + ItemName = itemName; + Value = value; + OldValue = oldValue; + } + + /// + /// Gets or sets the name of the updated item. + /// + public string ItemName + { + get; + set; + } + + /// + /// Gets or sets the old item value. + /// + public string OldValue + { + get; + set; + } + + /// + /// Gets or sets the updated value. + /// + public string Value + { + get; + set; + } +} diff --git a/src/openHAB.Core.Client/Messages/TriggerCommandMessage.cs b/src/openHAB.Core.Client/Messages/TriggerCommandMessage.cs new file mode 100644 index 00000000..292ab820 --- /dev/null +++ b/src/openHAB.Core.Client/Messages/TriggerCommandMessage.cs @@ -0,0 +1,48 @@ +using System; +using openHAB.Core.Client.Models; + +namespace openHAB.Core.Client.Messages; + +/// +/// Message that gets fired whenever a widget triggers a command. +/// +public class TriggerCommandMessage +{ + /// + /// Initializes a new instance of the class. + /// + /// The OpenHAB item that triggered the command. + /// The command that was triggered. + public TriggerCommandMessage(Item item, string command) + { + Id = Guid.NewGuid(); + Item = item; + Command = command; + } + + /// + /// Gets or sets the Command that was triggered. + /// + public string Command + { + get; + set; + } + + /// Gets the unique message identifier. + /// The identifier. + public Guid Id + { + get; + private set; + } + + /// + /// Gets or sets the OpenHAB item that triggered the command. + /// + public Item Item + { + get; + set; + } +} diff --git a/src/openHAB.Core.Client/Messages/UpdateItemMessage.cs b/src/openHAB.Core.Client/Messages/UpdateItemMessage.cs new file mode 100644 index 00000000..29f5bfe6 --- /dev/null +++ b/src/openHAB.Core.Client/Messages/UpdateItemMessage.cs @@ -0,0 +1,33 @@ +namespace openHAB.Core.Client.Messages; + +/// +/// A message to trigger an item update coming from a server event. +/// +public class UpdateItemMessage +{ + /// Initializes a new instance of the class. + /// Name of the item. + /// The value. + public UpdateItemMessage(string itemName, string value) + { + ItemName = itemName; + Value = value; + } + + /// + /// Gets or sets the name of the updated item. + /// + public string ItemName + { + get; + set; + } + + /// + /// Gets or sets the updated value. + /// + public string Value + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Models/APIInfo.cs b/src/openHAB.Core.Client/Models/APIInfo.cs new file mode 100644 index 00000000..c35666ab --- /dev/null +++ b/src/openHAB.Core.Client/Models/APIInfo.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace openHAB.Core.Client.Models; + +/// +/// Response for REST Root call, which provides all information about openHAB instance. +/// +public class APIInfo +{ + /// Gets or sets the version. + /// The version. + [JsonPropertyName("version")] + public string Version + { + get; set; + } + + /// Gets or sets the locale. + /// The locale. + [JsonPropertyName("locale")] + public string Locale + { + get; set; + } + + /// Gets or sets the measurement system. + /// The measurement system. + [JsonPropertyName("measurementSystem")] + public string MeasurementSystem + { + get; set; + } + + /// Gets or sets the runtime information. + /// The runtime information. + [JsonPropertyName("runtimeInfo")] + public RuntimeInfo RuntimeInfo + { + get; set; + } + + /// Gets or sets the links. + /// The links. + [JsonPropertyName("links")] + public List Links + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Models/CommandDescription.cs b/src/openHAB.Core.Client/Models/CommandDescription.cs new file mode 100644 index 00000000..a7b3493c --- /dev/null +++ b/src/openHAB.Core.Client/Models/CommandDescription.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace openHAB.Core.Client.Models; + +/// +/// A mapping for an OpenHAB Widget. +/// +public class CommandDescription +{ + /// + /// Gets or sets the CommandOptions. + /// + public ICollection CommandOptions + { + get; set; + } + + /// Initializes a new instance of the class. + /// The command options. + public CommandDescription(ICollection commandOptions) + { + CommandOptions = commandOptions; + } +} diff --git a/src/openHAB.Core.Client/Models/CommandOptions.cs b/src/openHAB.Core.Client/Models/CommandOptions.cs new file mode 100644 index 00000000..b801fca4 --- /dev/null +++ b/src/openHAB.Core.Client/Models/CommandOptions.cs @@ -0,0 +1,34 @@ +namespace openHAB.Core.Client.Models; + +/// +/// A CommandOptions for commandDescription. +/// +public class CommandOptions +{ + /// + /// Gets or sets the Command of the mapping. + /// + public string Command + { + get; set; + } + + /// + /// Gets or sets the Label of the mapping. + /// + public string Label + { + get; set; + } + + /// + /// Initializes a new instance of the class. + /// + /// A command. + /// A label. + public CommandOptions(string command, string label) + { + Command = command; + Label = label; + } +} diff --git a/src/openHAB.Core.Client/Models/Item.cs b/src/openHAB.Core.Client/Models/Item.cs new file mode 100644 index 00000000..bd09ade9 --- /dev/null +++ b/src/openHAB.Core.Client/Models/Item.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using openHAB.Core.Client.Messages; + +namespace openHAB.Core.Client.Models; + +/// +/// A class that represents an OpenHAB item. +/// +public class Item : ObservableObject +{ + private string _state; + private string _type; + + /// + /// Initializes a new instance of the class. + /// + public Item() + { + StrongReferenceMessenger.Default.Register(this, HandleUpdateItemMessage); + } + + /// + /// Initializes a new instance of the class. + /// + /// The XML from the OpenHAB server that represents this OpenHAB item. + public Item(XElement startNode) + { + ParseNode(startNode); + } + + /// + /// Initializes a new instance of the class. + /// + /// The JSON from the OpenHAB server that represents this OpenHAB item. + public Item(string jsonObject) + { + Item item = JsonSerializer.Deserialize(jsonObject); + Name = item.Name; + Type = item.Type; + GroupType = item.GroupType; + State = item.State; + Link = item.Link; + CommandDescription = item.CommandDescription; + } + + /// + /// Gets or sets the item category. + /// + /// The category. + [JsonPropertyName("category")] + public string Category + { + get; set; + } + + /// + /// Gets or sets the CommandDescription of the OpenHAB item. + /// + [JsonPropertyName("commandDescription")] + public CommandDescription CommandDescription + { + get; set; + } + + /// + /// Gets or sets a value indicating whether the item is editable. + /// + [JsonPropertyName("editable")] + public bool Editable + { + get; set; + } + + /// + /// Gets or sets the group names of the OpenHAB item. + /// + [JsonPropertyName("groupNames")] + public List GroupNames + { + get; set; + } + + /// + /// Gets or sets the grouptype of the OpenHAB item. + /// + public string GroupType + { + get; set; + } + + /// + /// Gets or sets the item label with the display name. + /// + /// The item label. + [JsonPropertyName("label")] + public string Label + { + get; set; + } + + /// + /// Gets or sets the unit of the OpenHAB item. + /// + [JsonPropertyName("link")] + public string Link + { + get; set; + } + + /// + /// Gets or sets the metadata of the OpenHAB item. + /// + [JsonPropertyName("metadata")] + public Metadata Metadata + { + get; set; + } + + /// + /// Gets or sets the name of the OpenHAB item. + /// + [JsonPropertyName("name")] + public string Name + { + get; set; + } + + /// + /// Gets or sets the state of the OpenHAB item. + /// + [JsonPropertyName("state")] + public string State + { + get => _state; + set + { + if (_type != null && Unit == null && _type.Contains(":", StringComparison.OrdinalIgnoreCase) && value != null && value.Contains(" ")) + { + int spaceIndex = value.LastIndexOf(' '); + Unit = value.Substring(spaceIndex, value.Length - spaceIndex); + } + + SetProperty(ref _state, value); + } + } + + [JsonPropertyName("stateDescription")] + public StateDescription StateDescription + { + get; set; + } + + [JsonPropertyName("tags")] + public List Tags + { + get; set; + } + + [JsonPropertyName("transformedState")] + public string TransformedState + { + get; set; + } + + /// + /// Gets or sets the type of the OpenHAB item. + /// + [JsonPropertyName("type")] + public string Type + { + get => _type; + set + { + if (value != null && value.Contains(":", StringComparison.OrdinalIgnoreCase) && _state != null) + { + int spaceIndex = _state.LastIndexOf(' '); + if (spaceIndex > 0) + { + Unit = _state.Substring(spaceIndex, _state.Length - spaceIndex); + } + } + + SetProperty(ref _type, value); + } + } + + /// + /// Gets or sets the link of the OpenHAB item. + /// + public string Unit + { + get; set; + } + + [JsonPropertyName("unitSymbol")] + public string UnitSymbol + { + get; set; + } + + /// + /// Convert state to double value. + /// + /// + /// State as double value. + /// + public double GetStateAsDoubleValue() + { + string newstate = Regex.Replace(_state, "[^0-9,.]", string.Empty, RegexOptions.None, TimeSpan.FromMilliseconds(100)); + double value = 0; + double.TryParse(newstate, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out value); + + return value; + } + + /// + /// Send update message to all subscribers. + /// + /// The value. + public void UpdateValue(object value) + { + if (value != null) + { + string newValue = value.ToString() + Unit; + StrongReferenceMessenger.Default.Send(new TriggerCommandMessage(this, newValue)); + _state = newValue; + } + } + + private async void HandleUpdateItemMessage(object recipient, UpdateItemMessage message) + { + if (message.ItemName != Name) + { + return; + } + + State = message.Value; + } + + private void ParseNode(XElement startNode) + { + if (!startNode.HasElements) + { + return; + } + + Name = startNode.Element("name")?.Value; + Type = startNode.Element("type")?.Value; + GroupType = startNode.Element("groupType")?.Value; + State = startNode.Element("state")?.Value; + Link = startNode.Element("link")?.Value; + } +} diff --git a/src/openHAB.Core.Client/Models/Link.cs b/src/openHAB.Core.Client/Models/Link.cs new file mode 100644 index 00000000..b39f526f --- /dev/null +++ b/src/openHAB.Core.Client/Models/Link.cs @@ -0,0 +1,24 @@ +using System; +using System.Text.Json.Serialization; + +namespace openHAB.Core.Client.Models; + +/// Use to serialize api information from openHAB server. +public partial class Link +{ + /// Gets or sets the type. + /// The type. + [JsonPropertyName("type")] + public string Type + { + get; set; + } + + /// Gets or sets the URL. + /// The URL. + [JsonPropertyName("url")] + public Uri Url + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Models/Metadata.cs b/src/openHAB.Core.Client/Models/Metadata.cs new file mode 100644 index 00000000..1ecba4e5 --- /dev/null +++ b/src/openHAB.Core.Client/Models/Metadata.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace openHAB.Core.Client.Models; + +/// +/// Represents the metadata for an object. +/// +public class Metadata +{ + /// + /// Gets or sets the value of additional property 1. + /// + [JsonPropertyName("additionalProp1")] + public object AdditionalProp1 + { + get; set; + } + + /// + /// Gets or sets the value of additional property 2. + /// + [JsonPropertyName("additionalProp2")] + public object AdditionalProp2 + { + get; set; + } + + /// + /// Gets or sets the value of additional property 3. + /// + [JsonPropertyName("additionalProp3")] + public object AdditionalProp3 + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Models/OpenHABException.cs b/src/openHAB.Core.Client/Models/OpenHABException.cs new file mode 100644 index 00000000..f67e36bd --- /dev/null +++ b/src/openHAB.Core.Client/Models/OpenHABException.cs @@ -0,0 +1,35 @@ +using System; + +namespace openHAB.Core.Client.Models; + +/// +/// An Exception class used to throw unexpected errors. +/// +public class OpenHABException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The error message. + public OpenHABException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message. + /// The original exception. + public OpenHABException(string message, Exception exception) + : base(message, exception) + { + } + + /// + /// Initializes a new instance of the class. + /// + public OpenHABException() + { + } +} diff --git a/src/openHAB.Core.Client/Models/OpenHabVersion.cs b/src/openHAB.Core.Client/Models/OpenHabVersion.cs new file mode 100644 index 00000000..011fcea3 --- /dev/null +++ b/src/openHAB.Core.Client/Models/OpenHabVersion.cs @@ -0,0 +1,32 @@ +namespace openHAB.Core.Client.Models; + +/// +/// Enum to differentiate between OpenHAB 1, 2 and 3. +/// +public enum OpenHABVersion +{ + /// + /// OpenHAB V1 + /// + One = 1, + + /// + /// OpenHAB V2 + /// + Two = 2, + + /// + /// OpenHAB V3 + /// + Three = 3, + + /// + /// OpenHAB V4 + /// + Four = 4, + + /// + /// Used when no connection is available + /// + None, +} diff --git a/src/openHAB.Core.Client/Models/Option.cs b/src/openHAB.Core.Client/Models/Option.cs new file mode 100644 index 00000000..3708d35e --- /dev/null +++ b/src/openHAB.Core.Client/Models/Option.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace openHAB.Core.Client.Models; + +/// +/// Represents an option with a value and label. +/// +public class Option +{ + /// + /// Gets or sets the value of the option. + /// + [JsonPropertyName("value")] + public string Value + { + get; set; + } + + /// + /// Gets or sets the label of the option. + /// + [JsonPropertyName("label")] + public string Label + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Models/Page.cs b/src/openHAB.Core.Client/Models/Page.cs new file mode 100644 index 00000000..363686c5 --- /dev/null +++ b/src/openHAB.Core.Client/Models/Page.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace openHAB.Core.Client.Models; + +/// +/// Represents a page in the openHAB application. +/// +public class Page +{ + /// + /// Gets or sets the ID of the page. + /// + [JsonPropertyName("id")] + public string Id + { + get; set; + } + + /// + /// Gets or sets the title of the page. + /// + [JsonPropertyName("title")] + public string Title + { + get; set; + } + + /// + /// Gets or sets the icon of the page. + /// + [JsonPropertyName("icon")] + public string Icon + { + get; set; + } + + /// + /// Gets or sets the link of the page. + /// + [JsonPropertyName("link")] + public string Link + { + get; set; + } + + /// + /// Gets or sets the parent page ID. + /// + [JsonPropertyName("parent")] + public string Parent + { + get; set; + } + + /// + /// Gets or sets a value indicating whether the page is a leaf page. + /// + [JsonPropertyName("leaf")] + public bool Leaf + { + get; set; + } + + /// + /// Gets or sets a value indicating whether the page has a timeout. + /// + [JsonPropertyName("timeout")] + public bool Timeout + { + get; set; + } + + /// + /// Gets or sets the list of widgets on the page. + /// + [JsonPropertyName("widgets")] + public List Widgets + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Models/RuntimeInfo.cs b/src/openHAB.Core.Client/Models/RuntimeInfo.cs new file mode 100644 index 00000000..6c277721 --- /dev/null +++ b/src/openHAB.Core.Client/Models/RuntimeInfo.cs @@ -0,0 +1,25 @@ + + +using System.Text.Json.Serialization; + +namespace openHAB.Core.Client.Models; + +/// Used to serialize the service information from openHAB server. +public partial class RuntimeInfo +{ + /// Gets or sets the server version. + /// The version. + [JsonPropertyName("version")] + public string Version + { + get; set; + } + + /// Gets or sets the build version string. + /// The build string. + [JsonPropertyName("buildString")] + public string BuildString + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Models/ServerInfo.cs b/src/openHAB.Core.Client/Models/ServerInfo.cs new file mode 100644 index 00000000..b71e8219 --- /dev/null +++ b/src/openHAB.Core.Client/Models/ServerInfo.cs @@ -0,0 +1,26 @@ +namespace openHAB.Core.Client.Models; + +/// Information about openHAB server. +public class ServerInfo +{ + /// Gets the openHAB major version. + /// The version. + public OpenHABVersion Version + { + get; set; + } + + /// Gets the build. + /// The build. + public string Build + { + get; set; + } + + /// Gets the runtime version. + /// The runtime version. + public string RuntimeVersion + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Models/Sitemap.cs b/src/openHAB.Core.Client/Models/Sitemap.cs new file mode 100644 index 00000000..2fc0c994 --- /dev/null +++ b/src/openHAB.Core.Client/Models/Sitemap.cs @@ -0,0 +1,55 @@ +using System.Text.Json.Serialization; + +namespace openHAB.Core.Client.Models; + + +/// +/// Represents an OpenHAB sitemap. +/// +public class Sitemap +{ + /// + /// Gets or sets the name of the sitemap. + /// + [JsonPropertyName("name")] + public string Name + { + get; set; + } + + /// + /// Gets or sets the icon of the sitemap. + /// + [JsonPropertyName("icon")] + public string Icon + { + get; set; + } + + /// + /// Gets or sets the label of the sitemap. + /// + [JsonPropertyName("label")] + public string Label + { + get; set; + } + + /// + /// Gets or sets the link of the sitemap. + /// + [JsonPropertyName("link")] + public string Link + { + get; set; + } + + /// + /// Gets or sets the homepage of the sitemap. + /// + [JsonPropertyName("homepage")] + public Page Homepage + { + get; set; + } +} diff --git a/src/openHAB.Core.Client/Models/StateDescription.cs b/src/openHAB.Core.Client/Models/StateDescription.cs new file mode 100644 index 00000000..0a471161 --- /dev/null +++ b/src/openHAB.Core.Client/Models/StateDescription.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace openHAB.Core.Client.Models; + + +/// +/// Represents the description of a state. +/// +public class StateDescription +{ + /// + /// Gets or sets the minimum value of the state. + /// + [JsonPropertyName("minimum")] + public float Minimum + { + get; set; + } + + /// + /// Gets or sets the maximum value of the state. + /// + [JsonPropertyName("maximum")] + public float Maximum + { + get; set; + } + + /// + /// Gets or sets the step value of the state. + /// + [JsonPropertyName("step")] + public float Step + { + get; set; + } + + /// + /// Gets or sets the pattern of the state. + /// + [JsonPropertyName("pattern")] + public string Pattern + { + get; set; + } + + /// + /// Gets or sets a value indicating whether the state is read-only. + /// + [JsonPropertyName("readOnly")] + public bool ReadOnly + { + get; set; + } + + /// + /// Gets or sets the list of options for the state. + /// + [JsonPropertyName("options")] + public List