├── js └── .gitkeep ├── img ├── screenshot1.jpg ├── screenshot2.jpg ├── yaml_widget-video.jpg ├── template_widget-video.jpg ├── logo-dark.svg └── app.svg ├── .gitattributes ├── .eslintrc.js ├── stylelint.config.js ├── .gitignore ├── .github └── workflows │ ├── reuse.yml │ ├── lint-info-xml.yml │ ├── lint-php-cs.yml │ ├── lint-php.yml │ ├── lint-eslint.yml │ ├── lint-stylelint.yml │ ├── phpunit-sqlite.yml │ ├── phpunit-oci.yml │ ├── phpunit-pgsql.yml │ └── phpunit-mysql.yml ├── .php_cs-fixer.dist.php ├── .reuse └── dep5 ├── appinfo ├── routes.php └── info.xml ├── lib ├── Sections │ └── HAssAdmin.php ├── AppInfo │ └── Application.php ├── Service │ └── HassIntegrationService.php ├── Listener │ └── AddContentSecurityPolicyListener.php ├── Controller │ └── HassIntegrationController.php ├── Settings │ └── AdminSettings.php └── Dashboard │ ├── YamlWidget.php │ └── TemplateWidget.php ├── src ├── templateWidget.js ├── admin-settings.js └── yamlWidget.js ├── composer.json ├── webpack.config.js ├── css ├── admin-settings.css └── dashboard.css ├── package.json ├── README.md ├── templates └── admin-settings.php ├── Makefile └── LICENSES ├── CC0-1.0.txt └── AGPL-3.0-or-later.txt /js/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poulou0/nextcloud-homeassistant-integration/HEAD/img/screenshot1.jpg -------------------------------------------------------------------------------- /img/screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poulou0/nextcloud-homeassistant-integration/HEAD/img/screenshot2.jpg -------------------------------------------------------------------------------- /img/yaml_widget-video.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poulou0/nextcloud-homeassistant-integration/HEAD/img/yaml_widget-video.jpg -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Poulou 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | /js/* binary 4 | -------------------------------------------------------------------------------- /img/template_widget-video.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poulou0/nextcloud-homeassistant-integration/HEAD/img/template_widget-video.jpg -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Poulou 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | module.exports = { 4 | extends: [ 5 | '@nextcloud', 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Nextcloud contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | const stylelintConfig = require('@nextcloud/stylelint-config') 4 | 5 | module.exports = stylelintConfig 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Poulou 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | .idea 4 | *.iml 5 | composer.lock 6 | /vendor/ 7 | /build/ 8 | node_modules/ 9 | /.php_cs.cache 10 | js/*hot-update.* 11 | js/*.js 12 | js/*.map 13 | js/*.txt -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | name: REUSE Compliance Check 6 | 7 | on: [push, pull_request] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: REUSE Compliance Check 15 | uses: fsfe/reuse-action@v1 -------------------------------------------------------------------------------- /.php_cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | 5 | // SPDX-License-Identifier: AGPL-3.0-or-later 6 | 7 | require_once './vendor/autoload.php'; 8 | 9 | use Nextcloud\CodingStandard\Config; 10 | 11 | $config = new Config(); 12 | $config 13 | ->getFinder() 14 | ->ignoreVCSIgnored(true) 15 | ->notPath('build') 16 | ->notPath('l10n') 17 | ->notPath('src') 18 | ->notPath('vendor') 19 | ->in(__DIR__); 20 | return $config; 21 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: Hass Integration 3 | Upstream-Contact: Poulou 4 | Source: https://github.com/nextcloud/profiler 5 | 6 | Files: package-lock.json package.json composer.json composer.lock lib/*/* css/* js/* src/* templates/* img/* 7 | Copyright: Poulou 8 | License: AGPL-3.0-or-later 9 | 10 | Files: l10n/*.js l10n/*.json 11 | Copyright: Nextcloud translators 12 | License: AGPL-3.0-or-later 13 | -------------------------------------------------------------------------------- /appinfo/routes.php: -------------------------------------------------------------------------------- 1 | 4 | // SPDX-License-Identifier: AGPL-3.0-or-later 5 | 6 | /** 7 | * Create your routes in here. The name is the lowercase name of the controller 8 | * without the controller part, the stuff after the hash is the method. 9 | * e.g. page#index -> OCA\HassIntegration\Controller\PageController->index() 10 | * 11 | * The controller class has to be registered in the application.php file since 12 | * it's instantiated in there 13 | */ 14 | return [ 15 | 'routes' => [ 16 | ['name' => 'hassIntegration#templatePost', 'url' => '/template', 'verb' => 'POST'], 17 | ['name' => 'hassIntegration#togglePost', 'url' => '/toggle', 'verb' => 'POST'], 18 | ['name' => 'hassIntegration#runScriptPost', 'url' => '/run_script', 'verb' => 'POST'], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /lib/Sections/HAssAdmin.php: -------------------------------------------------------------------------------- 1 | l = $l; 16 | $this->urlGenerator = $urlGenerator; 17 | } 18 | 19 | public function getIcon(): string { 20 | return $this->urlGenerator->imagePath(Application::APP_ID, 'logo-dark.svg'); 21 | } 22 | 23 | public function getID(): string { return self::SETTINGS_SECTION; } 24 | 25 | public function getName(): string { return $this->l->t('Home assistant integration'); } 26 | 27 | public function getPriority(): int { return 98; } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/lint-info-xml.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # SPDX-FileCopyrightText: Nextcloud contributors 6 | # SPDX-License-Identifier: AGPL-3.0-or-later 7 | 8 | name: Lint 9 | 10 | on: 11 | pull_request: 12 | push: 13 | branches: 14 | - main 15 | - master 16 | - stable* 17 | 18 | jobs: 19 | xml-linters: 20 | runs-on: ubuntu-latest 21 | 22 | name: info.xml lint 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@master 26 | 27 | - name: Download schema 28 | run: wget https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd 29 | 30 | - name: Lint info.xml 31 | uses: ChristophWurst/xmllint-action@v1 32 | with: 33 | xml-file: ./appinfo/info.xml 34 | xml-schema-file: ./info.xsd 35 | -------------------------------------------------------------------------------- /.github/workflows/lint-php-cs.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # SPDX-FileCopyrightText: Nextcloud contributors 6 | # SPDX-License-Identifier: AGPL-3.0-or-later 7 | 8 | name: Lint 9 | 10 | on: 11 | pull_request: 12 | push: 13 | branches: 14 | - master 15 | - stable* 16 | 17 | jobs: 18 | lint: 19 | runs-on: ubuntu-latest 20 | 21 | name: php-cs 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | 27 | - name: Set up php ${{ matrix.php-versions }} 28 | uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: "7.4" 31 | coverage: none 32 | 33 | - name: Install dependencies 34 | run: composer i 35 | 36 | - name: Lint 37 | run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 ) 38 | 39 | -------------------------------------------------------------------------------- /src/templateWidget.js: -------------------------------------------------------------------------------- 1 | import { loadState } from '@nextcloud/initial-state' 2 | import axios from '@nextcloud/axios' 3 | import { generateUrl } from '@nextcloud/router' 4 | 5 | document.addEventListener('DOMContentLoaded', () => { 6 | OCA.Dashboard.register('hass-template-widget', (el, { widget }) => { 7 | el.innerHTML = nl2br(loadState('integration_homeassistant', 'dashboard-template-widget')) 8 | if (!el.innerHTML) el.innerHTML = emptyMsg() 9 | el.parentElement.style.overflow = 'auto' 10 | const refreshInterval = parseInt(loadState('integration_homeassistant', 'dashboard-template-widget-refresh-interval')) 11 | if (refreshInterval > 0) { 12 | setInterval(async () => { 13 | el.innerHTML = nl2br((await axios.post(generateUrl('/apps/integration_homeassistant/template'))).data[0]) 14 | if (!el.innerHTML) el.innerHTML = emptyMsg() 15 | }, refreshInterval * 1000) 16 | } 17 | }) 18 | }) 19 | 20 | /** 21 | * Replace line breaks with
22 | * @param {string} s The variable to convert 23 | */ 24 | function nl2br(s) { 25 | return s.toString().replace(/(?:\r\n|\r|\n)/g, '
') 26 | } 27 | 28 | /** 29 | * Prints guide 30 | */ 31 | function emptyMsg() { return 'Nothing to show ::)

Go to "Administrator settings" > "Home assistant integration" to get started.' } 32 | -------------------------------------------------------------------------------- /lib/AppInfo/Application.php: -------------------------------------------------------------------------------- 1 | 4 | // SPDX-License-Identifier: AGPL-3.0-or-later 5 | 6 | namespace OCA\HassIntegration\AppInfo; 7 | 8 | use OCA\HassIntegration\Listener\AddContentSecurityPolicyListener; 9 | use OCA\HassIntegration\Dashboard\TemplateWidget; 10 | use OCA\HassIntegration\Dashboard\YamlWidget; 11 | use OCP\AppFramework\App; 12 | use OCP\AppFramework\Bootstrap\IRegistrationContext; 13 | use OCP\AppFramework\Bootstrap\IBootContext; 14 | use OCP\AppFramework\Bootstrap\IBootstrap; 15 | use OCP\Security\CSP\AddContentSecurityPolicyEvent; 16 | 17 | class Application extends App implements IBootstrap 18 | { 19 | public const APP_ID = 'integration_homeassistant'; 20 | 21 | public function __construct(array $urlParams = []) 22 | { 23 | parent::__construct(self::APP_ID, $urlParams); 24 | } 25 | 26 | public function register(IRegistrationContext $context): void 27 | { 28 | $context->registerDashboardWidget(TemplateWidget::class); 29 | $context->registerDashboardWidget(YamlWidget::class); 30 | 31 | $context->registerEventListener(AddContentSecurityPolicyEvent::class, AddContentSecurityPolicyListener::class); 32 | } 33 | 34 | public function boot(IBootContext $context): void 35 | { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/lint-php.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # SPDX-FileCopyrightText: Nextcloud contributors 6 | # SPDX-License-Identifier: AGPL-3.0-or-later 7 | 8 | name: Lint 9 | 10 | on: 11 | pull_request: 12 | push: 13 | branches: 14 | - main 15 | - master 16 | - stable* 17 | 18 | jobs: 19 | php-lint: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | php-versions: ["7.4", "8.0", "8.1"] 24 | 25 | name: php-lint 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v2 30 | 31 | - name: Set up php ${{ matrix.php-versions }} 32 | uses: shivammathur/setup-php@v2 33 | with: 34 | php-version: ${{ matrix.php-versions }} 35 | coverage: none 36 | 37 | - name: Lint 38 | run: composer run lint 39 | 40 | summary: 41 | runs-on: ubuntu-latest 42 | needs: php-lint 43 | 44 | if: always() 45 | 46 | name: php-lint-summary 47 | 48 | steps: 49 | - name: Summary status 50 | run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi 51 | 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextcloud/integration_homeassistant", 3 | "description": "A dashboard widget for a home-assistant instance", 4 | "type": "project", 5 | "license": "AGPL-3.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "Poulou" 9 | } 10 | ], 11 | "require-dev": { 12 | "phpunit/phpunit": "^9", 13 | "sabre/dav": "^4.1", 14 | "sabre/xml": "^2.2", 15 | "symfony/event-dispatcher": "^5.3.11", 16 | "psalm/phar": "^5.18", 17 | "nextcloud/coding-standard": "^1.0", 18 | "nextcloud/ocp": "^27.1" 19 | }, 20 | "scripts": { 21 | "lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l", 22 | "cs:check": "php-cs-fixer fix --dry-run --diff", 23 | "cs:fix": "php-cs-fixer fix", 24 | "psalm": "psalm.phar --threads=1", 25 | "psalm:update-baseline": "psalm.phar --threads=1 --update-baseline", 26 | "psalm:update-baseline:force": "psalm.phar --threads=1 --update-baseline --set-baseline=tests/psalm-baseline.xml", 27 | "psalm:clear": "psalm.phar --clear-cache && psalm --clear-global-cache", 28 | "psalm:fix": "psalm.phar --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType" 29 | }, 30 | "config": { 31 | "allow-plugins": { 32 | "composer/package-versions-deprecated": true 33 | }, 34 | "platform": { 35 | "php": "7.4" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/lint-eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # SPDX-FileCopyrightText: Nextcloud contributors 6 | # SPDX-License-Identifier: AGPL-3.0-or-later 7 | 8 | name: Lint 9 | 10 | on: 11 | pull_request: 12 | push: 13 | branches: 14 | - main 15 | - master 16 | - stable* 17 | 18 | jobs: 19 | lint: 20 | runs-on: ubuntu-latest 21 | 22 | name: eslint 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | 28 | - name: Read package.json node and npm engines version 29 | uses: skjnldsv/read-package-engines-version-actions@v1.2 30 | id: versions 31 | with: 32 | fallbackNode: '^12' 33 | fallbackNpm: '^6' 34 | 35 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 36 | uses: actions/setup-node@v3 37 | with: 38 | node-version: ${{ steps.versions.outputs.nodeVersion }} 39 | 40 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 41 | run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" 42 | 43 | - name: Install dependencies 44 | run: npm ci 45 | 46 | - name: Lint 47 | run: npm run lint 48 | -------------------------------------------------------------------------------- /.github/workflows/lint-stylelint.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # SPDX-FileCopyrightText: Nextcloud contributors 6 | # SPDX-License-Identifier: AGPL-3.0-or-later 7 | 8 | name: Lint 9 | 10 | on: 11 | pull_request: 12 | push: 13 | branches: 14 | - main 15 | - master 16 | - stable* 17 | 18 | jobs: 19 | lint: 20 | runs-on: ubuntu-latest 21 | 22 | name: stylelint 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | 28 | - name: Read package.json node and npm engines version 29 | uses: skjnldsv/read-package-engines-version-actions@v1.1 30 | id: versions 31 | with: 32 | fallbackNode: '^12' 33 | fallbackNpm: '^6' 34 | 35 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 36 | uses: actions/setup-node@v2 37 | with: 38 | node-version: ${{ steps.versions.outputs.nodeVersion }} 39 | 40 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 41 | run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" 42 | 43 | - name: Install dependencies 44 | run: npm ci 45 | 46 | - name: Lint 47 | run: npm run stylelint 48 | -------------------------------------------------------------------------------- /lib/Service/HassIntegrationService.php: -------------------------------------------------------------------------------- 1 | config = $config; 18 | $this->clientService = $clientService; 19 | } 20 | 21 | public function post(string $path, array $payload) 22 | { 23 | $baseURL = $this->config->getAppValue(Application::APP_ID, 'base_url', ''); 24 | if (substr($baseURL, -1) == '/') { 25 | $baseURL = substr($baseURL, 0, -1); 26 | } 27 | $longLivedAccessToken = $this->config->getAppValue(Application::APP_ID, 'long_lived_access_token', ''); 28 | $longLivedAccessToken = trim($longLivedAccessToken); 29 | 30 | if (!$baseURL || !$longLivedAccessToken) 31 | return []; 32 | 33 | $client = $this->clientService->newClient(); 34 | $response = $client->post($baseURL . '/api' . $path, [ 35 | 'headers' => [ 36 | 'Authorization' => 'Bearer ' . $longLivedAccessToken, 37 | 'Content-Type' => 'application/json', 38 | ], 39 | 'body' => json_encode($payload), 40 | ]); 41 | return [$response->getBody()]; 42 | } 43 | 44 | public function getYamlWidget(): array 45 | { 46 | return [$this->config->getAppValue(Application::APP_ID, 'yaml_widget', '')]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Poulou 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | const path = require('path') 4 | // we extend the Nextcloud webpack config 5 | const webpackConfig = require('@nextcloud/webpack-vue-config') 6 | // this is to enable eslint and stylelint during compilation 7 | const ESLintPlugin = require('eslint-webpack-plugin') 8 | const StyleLintPlugin = require('stylelint-webpack-plugin') 9 | 10 | const buildMode = process.env.NODE_ENV 11 | const isDev = buildMode === 'development' 12 | webpackConfig.devtool = isDev ? 'cheap-source-map' : 'source-map' 13 | 14 | webpackConfig.stats = { 15 | colors: true, 16 | modules: false, 17 | } 18 | 19 | const appId = 'integration_homeassistant' 20 | webpackConfig.entry = { 21 | templateWidget: { import: path.join(__dirname, 'src', 'templateWidget.js'), filename: appId + '-templateWidget.js' }, 22 | yamlWidget: { import: path.join(__dirname, 'src', 'yamlWidget.js'), filename: appId + '-yamlWidget.js' }, 23 | adminSettings: { import: path.join(__dirname, 'src', 'admin-settings.js'), filename: 'admin-settings.js' }, 24 | } 25 | 26 | // this enables eslint and stylelint during compilation 27 | webpackConfig.plugins.push( 28 | new ESLintPlugin({ 29 | extensions: ['js', 'vue'], 30 | files: 'src', 31 | failOnError: !isDev, 32 | }) 33 | ) 34 | webpackConfig.plugins.push( 35 | new StyleLintPlugin({ 36 | files: 'src/**/*.{css,scss,vue}', 37 | failOnError: !isDev, 38 | }), 39 | ) 40 | 41 | module.exports = webpackConfig 42 | -------------------------------------------------------------------------------- /css/admin-settings.css: -------------------------------------------------------------------------------- 1 | #integration_homeassistant.section h2 { 2 | margin-bottom: 0; 3 | } 4 | 5 | #integration_homeassistant label:not(.inline) { 6 | display: block; 7 | } 8 | 9 | #integration_homeassistant input, 10 | #integration_homeassistant textarea { 11 | width: 100%; 12 | max-width: 400px; 13 | } 14 | 15 | #integration_homeassistant textarea { 16 | min-height: 120px; 17 | font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace; 18 | } 19 | 20 | #integration_homeassistant .widget-section textarea { 21 | max-width: none; 22 | min-height: 300px; 23 | } 24 | 25 | 26 | #integration_homeassistant .widget-section input[type=checkbox] { 27 | width: 20px; 28 | display: inline-block; 29 | vertical-align: middle; 30 | height: auto; 31 | margin-inline-start: 5px; 32 | } 33 | 34 | 35 | #integration_homeassistant li { 36 | list-style: circle; 37 | margin-inline-start: 15px; 38 | } 39 | 40 | #integration_homeassistant .icon { 41 | vertical-align: text-bottom; 42 | } 43 | 44 | /* Style tabs */ 45 | .tablinks.active, 46 | .tablinks.active:active, 47 | .tablinks.active:hover, 48 | .tablinks.active:focus { 49 | background-color: var(--color-primary-element); 50 | color: var(--color-primary-element-text); 51 | } 52 | 53 | .tab { 54 | overflow: hidden; 55 | } 56 | 57 | .tab button { 58 | display: inline-block; 59 | } 60 | 61 | .tabcontent { 62 | display: none; 63 | padding: 20px 0 64 | } 65 | 66 | .tabcontent#jinja2_widget_tab { 67 | display: block; 68 | } 69 | 70 | #integration_homeassistant a { 71 | color: var(--color-primary-element) 72 | } -------------------------------------------------------------------------------- /appinfo/info.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | integration_homeassistant 9 | Home Assistant integration 10 | A jinja2 template and a YAML widget for a home-assistant instance 11 | 17 | 0.0.7 18 | agpl 19 | Poulou 20 | HassIntegration 21 | dashboard 22 | integration 23 | https://github.com/poulou0/nextcloud-homeassistant-integration 24 | https://github.com/poulou0/nextcloud-homeassistant-integration/issues 25 | https://github.com/poulou0/nextcloud-homeassistant-integration/raw/main/img/screenshot2.jpg 26 | 27 | 28 | 29 | 30 | OCA\HassIntegration\Settings\AdminSettings 31 | OCA\HassIntegration\Sections\HAssAdmin 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/Listener/AddContentSecurityPolicyListener.php: -------------------------------------------------------------------------------- 1 | config = $config; 24 | } 25 | 26 | /** 27 | * Add domains for websocket connections based on the base_url setting. 28 | * 29 | * @param Event $event 30 | */ 31 | public function handle(Event $event): void 32 | { 33 | if (!($event instanceof AddContentSecurityPolicyEvent)) { 34 | return; 35 | } 36 | 37 | $baseURL = $this->config->getAppValue(Application::APP_ID, 'base_url', ''); 38 | $url = parse_url($baseURL); 39 | 40 | if ($url && isset($url['host'])) { 41 | $hostWithPort = $url['host'] . (isset($url['port']) ? ':' . $url['port'] : ''); 42 | 43 | // Create a new policy instance 44 | $csp = new ContentSecurityPolicy(); 45 | 46 | // Add allowed WebSocket domains to the new policy 47 | $csp->addAllowedFrameDomain('\'self\''); 48 | $csp->addAllowedFrameAncestorDomain('\'self\''); 49 | $csp->addAllowedConnectDomain('ws://' . $hostWithPort); 50 | $csp->addAllowedConnectDomain('wss://' . $hostWithPort); 51 | 52 | // Add the new policy to the event 53 | $event->addPolicy($csp); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/Controller/HassIntegrationController.php: -------------------------------------------------------------------------------- 1 | config = $config; 21 | $this->hassIntegrationService = $hassIntegrationService; 22 | } 23 | 24 | /** 25 | * @NoAdminRequired 26 | * @NoCSRFRequired 27 | * 28 | * @return DataResponse 29 | */ 30 | public function templatePost() 31 | { 32 | $template = $this->config->getAppValue(Application::APP_ID, 'template_widget', ''); 33 | return new DataResponse($this->hassIntegrationService->post('/template', [ 34 | "template" => $template 35 | ])); 36 | } 37 | 38 | /** 39 | * @NoAdminRequired 40 | * @NoCSRFRequired 41 | * 42 | * @return DataResponse 43 | */ 44 | public function togglePost() 45 | { 46 | $entityId = $this->request->getParam("entity_id"); 47 | $pathPart = str_contains($entityId, 'switch') ? 'switch' : 'light'; 48 | return new DataResponse($this->hassIntegrationService->post("/services/{$pathPart}/toggle", [ 49 | "entity_id" => $entityId 50 | ])); 51 | } 52 | 53 | /** 54 | * @NoAdminRequired 55 | * @NoCSRFRequired 56 | * 57 | * @return DataResponse 58 | */ 59 | public function runScriptPost() 60 | { 61 | $entityId = $this->request->getParam("entity_id"); 62 | $scriptName = str_replace("script.", "", $entityId); 63 | return new DataResponse($this->hassIntegrationService->post("/services/script/{$scriptName}", [])); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/Settings/AdminSettings.php: -------------------------------------------------------------------------------- 1 | config = $config; 17 | } 18 | 19 | /** 20 | * @return TemplateResponse 21 | */ 22 | public function getForm() 23 | { 24 | return new TemplateResponse(Application::APP_ID, 'admin-settings', [ 25 | 'base_url' => $this->config->getAppValue(Application::APP_ID, 'base_url', ''), 26 | 'long_lived_access_token' => $this->config->getAppValue(Application::APP_ID, 'long_lived_access_token', ''), 27 | 'template_widget_title' => $this->config->getAppValue(Application::APP_ID, 'template_widget_title', ''), 28 | 'template_widget_refresh_interval' => $this->config->getAppValue(Application::APP_ID, 'template_widget_refresh_interval', 30), 29 | 'template_widget' => $this->config->getAppValue(Application::APP_ID, 'template_widget', ''), 30 | 'yaml_widget_title' => $this->config->getAppValue(Application::APP_ID, 'yaml_widget_title', ''), 31 | 'yaml_widget' => $this->config->getAppValue(Application::APP_ID, 'yaml_widget', ''), 32 | 'yaml_widget_websockets_enabled' => $this->config->getAppValue(Application::APP_ID, 'yaml_widget_websockets_enabled', 'true'), 33 | ], ''); 34 | } 35 | 36 | public function getSection() 37 | { 38 | return HAssAdmin::SETTINGS_SECTION; // Name of the previously created section. 39 | } 40 | 41 | /** 42 | * @return int whether the form should be rather on the top or bottom of 43 | * the admin section. The forms are arranged in ascending order of the 44 | * priority values. It is required to return a value between 0 and 100. 45 | * 46 | * E.g.: 70 47 | */ 48 | public function getPriority() 49 | { 50 | return 70; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/Dashboard/YamlWidget.php: -------------------------------------------------------------------------------- 1 | l10n = $l10n; 27 | $this->hassIntegrationService = $hassIntegrationService; 28 | $this->config = $config; 29 | $this->initialStateService = $initialStateService; 30 | } 31 | 32 | public function getId(): string 33 | { 34 | return 'hass-yaml-widget'; 35 | } 36 | public function getTitle(): string 37 | { 38 | $title = $this->config->getAppValue(Application::APP_ID, 'yaml_widget_title', ''); 39 | if (!$title) 40 | $title = 'YAML widget (beta)'; 41 | return $this->l10n->t($title); 42 | } 43 | public function getOrder(): int 44 | { 45 | return 11; 46 | } 47 | public function getIconClass(): string 48 | { 49 | return 'icon-hasswidget'; 50 | } 51 | public function getUrl(): ?string 52 | { 53 | return null; 54 | } 55 | 56 | public function load(): void 57 | { 58 | Util::addScript(Application::APP_ID, Application::APP_ID . '-yamlWidget'); 59 | Util::addStyle(Application::APP_ID, 'dashboard'); 60 | 61 | $this->initialStateService->provideInitialState('dashboard-yaml-widget', $this->getItems()[0]); 62 | 63 | $webSocketsEnabled = $this->config->getAppValue(Application::APP_ID, 'yaml_widget_websockets_enabled', 'true'); 64 | $this->initialStateService->provideInitialState('dashboard-yaml-widget-websockets-enabled', $webSocketsEnabled); 65 | } 66 | 67 | public function getItems(string $userId = null, ?string $since = null, int $limit = 7): array 68 | { 69 | return $this->hassIntegrationService->getYamlWidget(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration_homeassistant", 3 | "description": "A dashboard widget for a home-assistant instance", 4 | "version": "0.0.7", 5 | "author": "Poulou ", 6 | "contributors": [], 7 | "bugs": { 8 | "url": "https://github.com/poulou0/nextcloud-homeassistant-integration.git" 9 | }, 10 | "license": "agpl", 11 | "private": true, 12 | "scripts": { 13 | "build": "webpack --node-env production --progress", 14 | "dev": "webpack --node-env development --progress", 15 | "watch": "webpack --node-env development --progress --watch", 16 | "serve": "webpack --node-env development serve --progress", 17 | "lint": "eslint --ext .js,.vue src", 18 | "lint:fix": "eslint --ext .js,.vue src --fix", 19 | "stylelint": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue", 20 | "stylelint:fix": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue --fix" 21 | }, 22 | "dependencies": { 23 | "@nextcloud/axios": "^2.3.0", 24 | "@nextcloud/dialogs": "^6.2.0", 25 | "@nextcloud/initial-state": "^2.0.0", 26 | "@nextcloud/password-confirmation": "^5.0.1", 27 | "@nextcloud/router": "^3.0.1", 28 | "home-assistant-js-websocket": "^9.3.0", 29 | "yaml": "^2.4.2" 30 | }, 31 | "browserslist": [ 32 | "extends @nextcloud/browserslist-config" 33 | ], 34 | "engines": { 35 | "node": "^16.0.0", 36 | "npm": "^7.0.0 || ^8.0.0" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.21.4", 40 | "@nextcloud/babel-config": "^1.0.0", 41 | "@nextcloud/browserslist-config": "^3.0.0", 42 | "@nextcloud/eslint-config": "^8.2.1", 43 | "@nextcloud/eslint-plugin": "^2.0.0", 44 | "@nextcloud/stylelint-config": "^3.0.1", 45 | "@nextcloud/webpack-vue-config": "^5.5.1", 46 | "babel-loader": "^9.2.1", 47 | "eslint": "^8.57.1", 48 | "eslint-plugin-import": "^2.27.5", 49 | "eslint-plugin-jsdoc": "^46.10.1", 50 | "eslint-plugin-n": "^16.6.0", 51 | "eslint-plugin-vue": "^9.11.0", 52 | "eslint-webpack-plugin": "^4.0.1", 53 | "stylelint": "^16.6.0", 54 | "stylelint-webpack-plugin": "^5.0.1", 55 | "vue-loader": "^15.11.1", 56 | "webpack": "^5.79.0", 57 | "webpack-cli": "^5.0.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /css/dashboard.css: -------------------------------------------------------------------------------- 1 | .icon-hasswidget { 2 | background-image: url('../img/logo-dark.svg'); 3 | filter: var(--background-invert-if-dark); 4 | } 5 | 6 | /* 7 | * Switch toggle 8 | * https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_switch 9 | */ 10 | 11 | .switch { 12 | position: relative; 13 | display: flex; 14 | width: 52px; 15 | height: 16px; 16 | } 17 | 18 | .switch input { 19 | opacity: 0; 20 | width: 0; 21 | height: 0; 22 | } 23 | 24 | .slider { 25 | position: absolute; 26 | cursor: pointer; 27 | top: 0; 28 | inset-inline: 0; 29 | bottom: 0; 30 | background-color: #4c4c4c; 31 | -webkit-transition: .4s; 32 | transition: .4s; 33 | } 34 | 35 | input[disabled]+.slider { 36 | cursor: not-allowed; 37 | background-color: #2e2e2e; 38 | } 39 | 40 | input[disabled]+.slider:before { 41 | background-color: #4b4b4b; 42 | } 43 | 44 | .slider:before { 45 | position: absolute; 46 | content: ""; 47 | height: 26px; 48 | width: 26px; 49 | inset-inline-start: 0; 50 | bottom: 0; 51 | background-color: white; 52 | -webkit-transition: .4s; 53 | transition: .4s; 54 | transform: translateY(5px); 55 | } 56 | 57 | input:checked+.slider { 58 | background-color: #0e6891; 59 | } 60 | 61 | input:checked+.slider:before { 62 | background-color: #03a9f4; 63 | transform: translateX(26px) translateY(5px); 64 | } 65 | 66 | /* Rounded sliders */ 67 | .slider.round { 68 | border-radius: 34px; 69 | } 70 | 71 | .slider.round:before { 72 | border-radius: 50%; 73 | } 74 | 75 | /* 76 | * Yaml widget 77 | */ 78 | 79 | div.panel--content:has(> div[data-id="hass-yaml-widget"]) { 80 | overflow: auto; 81 | } 82 | 83 | .entity-line { 84 | display: flex; 85 | justify-content: space-between; 86 | align-items: center; 87 | margin-bottom: 5px; 88 | } 89 | 90 | .entity-line.entity, 91 | .entity-line.type-section, 92 | .entity-line.type-weblink { 93 | height: 40px; 94 | } 95 | 96 | .entity-line>div:nth-child(1) { 97 | flex: 14; 98 | min-width: 0; 99 | } 100 | 101 | .entity-line>div:nth-child(2) { 102 | flex: 6; 103 | min-width: 0; 104 | display: flex; 105 | justify-content: flex-end; 106 | } 107 | 108 | .entity-line>div>p { 109 | text-overflow: ellipsis; 110 | overflow: hidden; 111 | white-space: nowrap; 112 | } 113 | 114 | .type-section { 115 | font-weight: bold; 116 | } 117 | 118 | .type-weblink a { 119 | color: var(--color-primary-element); 120 | text-decoration: underline; 121 | } -------------------------------------------------------------------------------- /src/admin-settings.js: -------------------------------------------------------------------------------- 1 | import { showSuccess, showError } from '@nextcloud/dialogs' 2 | import { generateOcsUrl } from '@nextcloud/router' 3 | import axios from '@nextcloud/axios' 4 | import { confirmPassword } from '@nextcloud/password-confirmation' 5 | import '@nextcloud/dialogs/style.css' 6 | import '@nextcloud/password-confirmation/style.css' 7 | 8 | document.addEventListener('DOMContentLoaded', function() { 9 | const inputHandler = function(evt) { saveSetting(this.id, this.value) } 10 | document.querySelector('#base_url').addEventListener('change', inputHandler) 11 | document.querySelector('#long_lived_access_token').addEventListener('change', inputHandler) 12 | document.querySelector('#template_widget_title').addEventListener('change', inputHandler) 13 | document.querySelector('#template_widget').addEventListener('change', inputHandler) 14 | document.querySelector('#template_widget_refresh_interval').addEventListener('change', inputHandler) 15 | document.querySelector('#yaml_widget_title').addEventListener('change', inputHandler) 16 | document.querySelector('#yaml_widget').addEventListener('change', inputHandler) 17 | document.querySelector('#yaml_widget_websockets_enabled').addEventListener('change', ({ target: { id, checked } }) => { 18 | saveSetting(id, checked) 19 | }) 20 | 21 | setupTabs() 22 | }) 23 | 24 | /** 25 | * Save a setting by name. 26 | * 27 | * @param {string} key The name/id of the field. 28 | * @param {string} value The value to be set. 29 | * @return {void} 30 | */ 31 | const saveSetting = async (key, value) => { 32 | try { 33 | await confirmPassword() 34 | const url = generateOcsUrl(`/apps/provisioning_api/api/v1/config/apps/integration_homeassistant/${key}`) 35 | await axios.post(url, new URLSearchParams({ value }).toString()) 36 | showSuccess(`Saved '${key}'!`) 37 | } catch (er) { 38 | showError(`Error while saving '${key}'`) 39 | } 40 | } 41 | 42 | /** 43 | * Setup the admin panel tabs. 44 | * 45 | * @return {void} 46 | */ 47 | const setupTabs = () => { 48 | const tablinks = document.querySelectorAll('.tablinks') 49 | const tabcontent = document.querySelectorAll('.tabcontent') 50 | const openTab = (evt, tabId) => { 51 | tablinks.forEach((link) => link.classList.remove('active')) 52 | tabcontent.forEach((content) => { content.style.display = 'none' }) 53 | document.querySelector(`#${tabId}`).style.display = 'block' 54 | evt.currentTarget.classList.add('active') 55 | } 56 | for (let i = 0; i < tablinks.length; i++) { 57 | tablinks[i].addEventListener('click', function(e) { openTab(e, tablinks[i].dataset.target) }, false) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/Dashboard/TemplateWidget.php: -------------------------------------------------------------------------------- 1 | l10n = $l10n; 26 | $this->hassIntegrationService = $hassIntegrationService; 27 | $this->config = $config; 28 | $this->initialStateService = $initialStateService; 29 | } 30 | 31 | public function getId(): string { return 'hass-template-widget'; } 32 | public function getTitle(): string { 33 | $title = $this->config->getAppValue(Application::APP_ID, 'template_widget_title', ''); 34 | if (!$title) $title = 'Template widget'; 35 | return $this->l10n->t($title); 36 | } 37 | public function getOrder(): int { return 10; } 38 | public function getIconClass(): string { return 'icon-hasswidget'; } 39 | public function getUrl(): ?string { return null; } 40 | 41 | public function load(): void 42 | { 43 | $baseURL = $this->config->getAppValue(Application::APP_ID, 'base_url', ''); 44 | $this->initialStateService->provideInitialState('dashboard-base-url', $baseURL); 45 | $longLivedAccessToken = $this->config->getAppValue(Application::APP_ID, 'long_lived_access_token', ''); 46 | $this->initialStateService->provideInitialState('dashboard-long-lived-access-token', $longLivedAccessToken); 47 | 48 | $items = $this->getItems(); 49 | $this->initialStateService->provideInitialState('dashboard-template-widget', $items); 50 | $interval = (int) $this->config->getAppValue(Application::APP_ID, 'template_widget_refresh_interval', 30); 51 | $this->initialStateService->provideInitialState('dashboard-template-widget-refresh-interval', $interval); 52 | 53 | Util::addScript(Application::APP_ID, Application::APP_ID . '-templateWidget'); 54 | Util::addStyle(Application::APP_ID, 'dashboard'); 55 | } 56 | 57 | public function getItems(string $userId = null, ?string $since = null, int $limit = 7): array { 58 | $template = $this->config->getAppValue(Application::APP_ID, 'template_widget', ''); 59 | return $this->hassIntegrationService->post('/template', [ 60 | "template" => $template 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/phpunit-sqlite.yml: -------------------------------------------------------------------------------- 1 | 2 | # SPDX-FileCopyrightText: Nextcloud contributors 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | name: PHPUnit 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | - master 12 | - stable* 13 | 14 | env: 15 | # Location of the phpunit.xml and phpunit.integration.xml files 16 | PHPUNIT_CONFIG: ./tests/phpunit.xml 17 | PHPUNIT_INTEGRATION_CONFIG: ./tests/phpunit.integration.xml 18 | 19 | jobs: 20 | phpunit-sqlite: 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | matrix: 25 | php-versions: ['8.1'] 26 | server-versions: ['master'] 27 | 28 | steps: 29 | - name: Set app env 30 | run: | 31 | # Split and keep last 32 | echo "APP_NAME=integration_homeassistant" >> $GITHUB_ENV 33 | 34 | - name: Checkout server 35 | uses: actions/checkout@v3 36 | with: 37 | submodules: true 38 | repository: nextcloud/server 39 | ref: ${{ matrix.server-versions }} 40 | 41 | - name: Checkout app 42 | uses: actions/checkout@v3 43 | with: 44 | path: apps/${{ env.APP_NAME }} 45 | 46 | - name: Set up php ${{ matrix.php-versions }} 47 | uses: shivammathur/setup-php@v2 48 | with: 49 | php-version: ${{ matrix.php-versions }} 50 | tools: phpunit 51 | extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite 52 | coverage: none 53 | 54 | - name: Set up PHPUnit 55 | working-directory: apps/${{ env.APP_NAME }} 56 | run: composer i 57 | 58 | - name: Set up Nextcloud 59 | env: 60 | DB_PORT: 4444 61 | run: | 62 | mkdir data 63 | ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password 64 | ./occ app:enable ${{ env.APP_NAME }} 65 | 66 | - name: Check PHPUnit config file existence 67 | id: check_phpunit 68 | uses: andstor/file-existence-action@v1 69 | with: 70 | files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_CONFIG }} 71 | 72 | - name: PHPUnit 73 | # Only run if phpunit config file exists 74 | if: steps.check_phpunit.outputs.files_exists == 'true' 75 | working-directory: apps/${{ env.APP_NAME }} 76 | run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_CONFIG }} 77 | 78 | - name: Check PHPUnit integration config file existence 79 | id: check_integration 80 | uses: andstor/file-existence-action@v1 81 | with: 82 | files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_INTEGRATION_CONFIG }} 83 | 84 | - name: Run Nextcloud 85 | # Only run if phpunit integration config file exists 86 | if: steps.check_integration.outputs.files_exists == 'true' 87 | run: php -S localhost:8080 & 88 | 89 | - name: PHPUnit integration 90 | # Only run if phpunit integration config file exists 91 | if: steps.check_integration.outputs.files_exists == 'true' 92 | working-directory: apps/${{ env.APP_NAME }} 93 | run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_INTEGRATION_CONFIG }} 94 | 95 | summary: 96 | runs-on: ubuntu-latest 97 | needs: phpunit-sqlite 98 | 99 | if: always() 100 | 101 | name: phpunit-sqlite-summary 102 | 103 | steps: 104 | - name: Summary status 105 | run: if ${{ needs.phpunit-sqlite.result != 'success' }}; then exit 1; fi 106 | 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ![Home Assistant Integration, dasboard widget screenshot](img/screenshot2.jpg) 7 | 8 | # Home Assistant Integration 9 | Home assistant widgets for your nextcloud dashboard. 10 | 11 | | Jinja2 template widget | YAML widget | 12 | | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | 13 | | [![Jinja2 template widget](img/template_widget-video.jpg)](https://youtu.be/XE_LRTAfVjA) | [![YAML widget](img/yaml_widget-video.jpg)](https://youtu.be/PjWH53rqYe8) | 14 | 15 | ## Development environment 16 | 17 | ### Nextcloud dev 18 | > https://github.com/juliushaertl/nextcloud-docker-dev#simple-master-setup 19 | ```shell 20 | cd ~/Projects 21 | git clone https://github.com/juliushaertl/nextcloud-docker-dev 22 | cd nextcloud-docker-dev 23 | ./bootstrap.sh 24 | docker-compose up nextcloud 25 | ``` 26 | go to `http://nextcloud.local` (`admin:admin` for credentials) 27 | ### App dev 28 | 29 | ```shell 30 | sudo apt install php php-xml php-curl composer 31 | ``` 32 | ```shell 33 | cd ~/Projects/nextcloud-docker-dev 34 | git clone git@github.com:poulou0/nextcloud-homeassistant-integration.git workspace/server/apps-extra/integration_homeassistant/ 35 | cd workspace/server/apps-extra/integration_homeassistant/ 36 | npm install 37 | make 38 | npm run watch 39 | ``` 40 | 41 | ## Publish to App Store 42 | 43 | First get an account for the [App Store](http://apps.nextcloud.com/) then run: 44 | 45 | make && make appstore 46 | 47 | The archive is located in build/artifacts/appstore and can then be uploaded to the App Store. 48 | 49 | Post it here according to the instructions: https://apps.nextcloud.com/developer/apps/releases/new 50 | 51 | ```shell 52 | openssl dgst -sha512 -sign ~/.nextcloud/certificates/integration_homeassistant.key ~/Projects/nextcloud-docker-dev/workspace/server/apps-extra/integration_homeassistant/build/artifacts/appstore/integration_homeassistant.tar.gz | openssl base64 53 | ``` 54 | 55 | ## Building the app 56 | 57 | The app can be built by using the provided Makefile by running: 58 | 59 | make 60 | 61 | This requires the following things to be present: 62 | * make 63 | * which 64 | * tar: for building the archive 65 | * curl: used if phpunit and composer are not installed to fetch them from the web 66 | * npm: for building and testing everything JS, only required if a package.json is placed inside the **js/** folder 67 | 68 | The make command will install or update Composer dependencies if a composer.json is present and also **npm run build** if a package.json is present in the **js/** folder. The npm **build** script should use local paths for build systems and package managers, so people that simply want to build the app won't need to install npm libraries globally, e.g.: 69 | 70 | **package.json**: 71 | ```json 72 | "scripts": { 73 | "test": "node node_modules/gulp-cli/bin/gulp.js karma", 74 | "prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update", 75 | "build": "node node_modules/gulp-cli/bin/gulp.js" 76 | } 77 | ``` 78 | 79 | ## Running tests 80 | You can use the provided Makefile to run all tests by using: 81 | 82 | make test 83 | 84 | This will run the PHP unit and integration tests and if a package.json is present in the **js/** folder will execute **npm run test** 85 | 86 | Of course you can also install [PHPUnit](http://phpunit.de/getting-started.html) and use the configurations directly: 87 | 88 | phpunit -c phpunit.xml 89 | 90 | or: 91 | 92 | phpunit -c phpunit.integration.xml 93 | 94 | for integration tests 95 | -------------------------------------------------------------------------------- /.github/workflows/phpunit-oci.yml: -------------------------------------------------------------------------------- 1 | 2 | # SPDX-FileCopyrightText: Nextcloud contributors 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | name: PHPUnit 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | - master 12 | - stable* 13 | 14 | env: 15 | # Location of the phpunit.xml and phpunit.integration.xml files 16 | PHPUNIT_CONFIG: ./tests/phpunit.xml 17 | PHPUNIT_INTEGRATION_CONFIG: ./tests/phpunit.integration.xml 18 | 19 | jobs: 20 | phpunit-oci: 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | matrix: 25 | php-versions: ['8.1'] 26 | server-versions: ['master'] 27 | 28 | services: 29 | oracle: 30 | image: oracleinanutshell/oracle-xe-11g 31 | ports: 32 | - 1521:1521/tcp 33 | 34 | steps: 35 | - name: Set app env 36 | run: | 37 | # Split and keep last 38 | echo "APP_NAME=integration_homeassistant" >> $GITHUB_ENV 39 | 40 | - name: Checkout server 41 | uses: actions/checkout@v3 42 | with: 43 | submodules: true 44 | repository: nextcloud/server 45 | ref: ${{ matrix.server-versions }} 46 | 47 | - name: Checkout app 48 | uses: actions/checkout@v3 49 | with: 50 | path: apps/${{ env.APP_NAME }} 51 | 52 | - name: Set up php ${{ matrix.php-versions }} 53 | uses: shivammathur/setup-php@v2 54 | with: 55 | php-version: ${{ matrix.php-versions }} 56 | extensions: mbstring, fileinfo, intl, sqlite, pdo_sqlite, oci8 57 | tools: phpunit 58 | coverage: none 59 | 60 | - name: Set up PHPUnit 61 | working-directory: apps/${{ env.APP_NAME }} 62 | run: composer i 63 | 64 | - name: Set up Nextcloud 65 | env: 66 | DB_PORT: 1521 67 | run: | 68 | mkdir data 69 | ./occ maintenance:install --verbose --database=oci --database-name=XE --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=system --database-pass=oracle --admin-user admin --admin-pass admin 70 | ./occ app:enable ${{ env.APP_NAME }} 71 | 72 | - name: Check PHPUnit config file existence 73 | id: check_phpunit 74 | uses: andstor/file-existence-action@v1 75 | with: 76 | files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_CONFIG }} 77 | 78 | - name: PHPUnit 79 | # Only run if phpunit config file exists 80 | if: steps.check_phpunit.outputs.files_exists == 'true' 81 | working-directory: apps/${{ env.APP_NAME }} 82 | run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_CONFIG }} 83 | 84 | - name: Check PHPUnit integration config file existence 85 | id: check_integration 86 | uses: andstor/file-existence-action@v1 87 | with: 88 | files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_INTEGRATION_CONFIG }} 89 | 90 | - name: Run Nextcloud 91 | # Only run if phpunit integration config file exists 92 | if: steps.check_integration.outputs.files_exists == 'true' 93 | run: php -S localhost:8080 & 94 | 95 | - name: PHPUnit integration 96 | # Only run if phpunit integration config file exists 97 | if: steps.check_integration.outputs.files_exists == 'true' 98 | working-directory: apps/${{ env.APP_NAME }} 99 | run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_INTEGRATION_CONFIG }} 100 | 101 | summary: 102 | runs-on: ubuntu-latest 103 | needs: phpunit-oci 104 | 105 | if: always() 106 | 107 | name: phpunit-oci-summary 108 | 109 | steps: 110 | - name: Summary status 111 | run: if ${{ needs.phpunit-oci.result != 'success' }}; then exit 1; fi 112 | 113 | -------------------------------------------------------------------------------- /.github/workflows/phpunit-pgsql.yml: -------------------------------------------------------------------------------- 1 | 2 | # SPDX-FileCopyrightText: Nextcloud contributors 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | name: PHPUnit 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | - master 12 | - stable* 13 | 14 | env: 15 | # Location of the phpunit.xml and phpunit.integration.xml files 16 | PHPUNIT_CONFIG: ./tests/phpunit.xml 17 | PHPUNIT_INTEGRATION_CONFIG: ./tests/phpunit.integration.xml 18 | 19 | jobs: 20 | phpunit-pgsql: 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | matrix: 25 | php-versions: ['8.1'] 26 | server-versions: ['master'] 27 | 28 | services: 29 | postgres: 30 | image: postgres 31 | ports: 32 | - 4444:5432/tcp 33 | env: 34 | POSTGRES_USER: root 35 | POSTGRES_PASSWORD: rootpassword 36 | POSTGRES_DB: nextcloud 37 | options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5 38 | 39 | steps: 40 | - name: Set app env 41 | run: | 42 | # Split and keep last 43 | echo "APP_NAME=integration_homeassistant" >> $GITHUB_ENV 44 | 45 | - name: Checkout server 46 | uses: actions/checkout@v3 47 | with: 48 | submodules: true 49 | repository: nextcloud/server 50 | ref: ${{ matrix.server-versions }} 51 | 52 | - name: Checkout app 53 | uses: actions/checkout@v3 54 | with: 55 | path: apps/${{ env.APP_NAME }} 56 | 57 | - name: Set up php ${{ matrix.php-versions }} 58 | uses: shivammathur/setup-php@v2 59 | with: 60 | php-version: ${{ matrix.php-versions }} 61 | tools: phpunit 62 | extensions: mbstring, iconv, fileinfo, intl, pgsql, pdo_pgsql 63 | coverage: none 64 | 65 | - name: Set up PHPUnit 66 | working-directory: apps/${{ env.APP_NAME }} 67 | run: composer i 68 | 69 | - name: Set up Nextcloud 70 | env: 71 | DB_PORT: 4444 72 | run: | 73 | mkdir data 74 | ./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password 75 | ./occ app:enable ${{ env.APP_NAME }} 76 | 77 | - name: Check PHPUnit config file existence 78 | id: check_phpunit 79 | uses: andstor/file-existence-action@v1 80 | with: 81 | files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_CONFIG }} 82 | 83 | - name: PHPUnit 84 | # Only run if phpunit config file exists 85 | if: steps.check_phpunit.outputs.files_exists == 'true' 86 | working-directory: apps/${{ env.APP_NAME }} 87 | run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_CONFIG }} 88 | 89 | - name: Check PHPUnit integration config file existence 90 | id: check_integration 91 | uses: andstor/file-existence-action@v1 92 | with: 93 | files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_INTEGRATION_CONFIG }} 94 | 95 | - name: Run Nextcloud 96 | # Only run if phpunit integration config file exists 97 | if: steps.check_integration.outputs.files_exists == 'true' 98 | run: php -S localhost:8080 & 99 | 100 | - name: PHPUnit integration 101 | # Only run if phpunit integration config file exists 102 | if: steps.check_integration.outputs.files_exists == 'true' 103 | working-directory: apps/${{ env.APP_NAME }} 104 | run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_INTEGRATION_CONFIG }} 105 | 106 | summary: 107 | runs-on: ubuntu-latest 108 | needs: phpunit-pgsql 109 | 110 | if: always() 111 | 112 | name: phpunit-pgsql-summary 113 | 114 | steps: 115 | - name: Summary status 116 | run: if ${{ needs.phpunit-pgsql.result != 'success' }}; then exit 1; fi 117 | 118 | -------------------------------------------------------------------------------- /.github/workflows/phpunit-mysql.yml: -------------------------------------------------------------------------------- 1 | 2 | # SPDX-FileCopyrightText: Nextcloud contributors 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | name: PHPUnit 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | - master 12 | - stable* 13 | 14 | env: 15 | # Location of the phpunit.xml and phpunit.integration.xml files 16 | PHPUNIT_CONFIG: ./tests/phpunit.xml 17 | PHPUNIT_INTEGRATION_CONFIG: ./tests/phpunit.integration.xml 18 | 19 | jobs: 20 | phpunit-mysql: 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | matrix: 25 | php-versions: ['8.1'] 26 | server-versions: ['master'] 27 | 28 | services: 29 | mysql: 30 | image: mariadb:10.5 31 | ports: 32 | - 4444:3306/tcp 33 | env: 34 | MYSQL_ROOT_PASSWORD: rootpassword 35 | options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 5 36 | 37 | steps: 38 | - name: Set app env 39 | run: | 40 | # Split and keep last 41 | echo "APP_NAME=integration_homeassistant" >> $GITHUB_ENV 42 | 43 | - name: Enable ONLY_FULL_GROUP_BY MySQL option 44 | run: | 45 | echo "SET GLOBAL sql_mode=(SELECT CONCAT(@@sql_mode,',ONLY_FULL_GROUP_BY'));" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword 46 | echo "SELECT @@sql_mode;" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword 47 | 48 | - name: Checkout server 49 | uses: actions/checkout@v3 50 | with: 51 | submodules: true 52 | repository: nextcloud/server 53 | ref: ${{ matrix.server-versions }} 54 | 55 | - name: Checkout app 56 | uses: actions/checkout@v3 57 | with: 58 | path: apps/${{ env.APP_NAME }} 59 | 60 | - name: Set up php ${{ matrix.php-versions }} 61 | uses: shivammathur/setup-php@v2 62 | with: 63 | php-version: ${{ matrix.php-versions }} 64 | tools: phpunit 65 | extensions: mbstring, iconv, fileinfo, intl, mysql, pdo_mysql 66 | coverage: none 67 | 68 | - name: Set up PHPUnit 69 | working-directory: apps/${{ env.APP_NAME }} 70 | run: composer i 71 | 72 | - name: Set up Nextcloud 73 | env: 74 | DB_PORT: 4444 75 | run: | 76 | mkdir data 77 | ./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password 78 | ./occ app:enable ${{ env.APP_NAME }} 79 | 80 | - name: Check PHPUnit config file existence 81 | id: check_phpunit 82 | uses: andstor/file-existence-action@v1 83 | with: 84 | files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_CONFIG }} 85 | 86 | - name: Run Nextcloud 87 | run: php -S localhost:8080 & 88 | 89 | - name: PHPUnit 90 | # Only run if phpunit config file exists 91 | if: steps.check_phpunit.outputs.files_exists == 'true' 92 | working-directory: apps/${{ env.APP_NAME }} 93 | run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_CONFIG }} 94 | 95 | - name: Check PHPUnit integration config file existence 96 | id: check_integration 97 | uses: andstor/file-existence-action@v1 98 | with: 99 | files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_INTEGRATION_CONFIG }} 100 | 101 | - name: PHPUnit integration 102 | # Only run if phpunit integration config file exists 103 | if: steps.check_integration.outputs.files_exists == 'true' 104 | working-directory: apps/${{ env.APP_NAME }} 105 | run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_INTEGRATION_CONFIG }} 106 | 107 | summary: 108 | runs-on: ubuntu-latest 109 | needs: phpunit-mysql 110 | 111 | if: always() 112 | 113 | name: phpunit-mysql-summary 114 | 115 | steps: 116 | - name: Summary status 117 | run: if ${{ needs.phpunit-mysql.result != 'success' }}; then exit 1; fi 118 | 119 | -------------------------------------------------------------------------------- /src/yamlWidget.js: -------------------------------------------------------------------------------- 1 | import { loadState } from '@nextcloud/initial-state' 2 | import { 3 | createConnection, 4 | subscribeEntities, 5 | createLongLivedTokenAuth, 6 | } from 'home-assistant-js-websocket' 7 | import YAML from 'yaml' 8 | import axios from '@nextcloud/axios' 9 | import { generateUrl } from '@nextcloud/router' 10 | 11 | document.addEventListener('DOMContentLoaded', () => { 12 | OCA.Dashboard.register('hass-yaml-widget', (el, { widget }) => { 13 | const webSocketsEnabled = loadState('integration_homeassistant', 'dashboard-yaml-widget-websockets-enabled') === 'true' 14 | try { 15 | const yamlEntities = YAML.parse(loadState('integration_homeassistant', 'dashboard-yaml-widget')) 16 | if (yamlEntities.type !== 'entities') { 17 | el.innerHTML = 'YAML is not of "type: entities"' 18 | } 19 | Object.values(yamlEntities.entities).forEach(entry => { 20 | if (entry.type === 'divider') { 21 | el.innerHTML += '

' 22 | } if (entry.type === 'section') { 23 | el.innerHTML += `
${entry.label}
` 24 | } if (entry.type === 'weblink') { 25 | el.innerHTML += `