diff --git a/.gitignore b/.gitignore
index d66c7cf..9ca868a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
vendor/
var/
-.env.local
\ No newline at end of file
+.env.local
+###> friendsofphp/php-cs-fixer ###
+/.php-cs-fixer.php
+/.php-cs-fixer.cache
+###< friendsofphp/php-cs-fixer ###
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..c9c7042
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,67 @@
+in([
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ])
+ ->exclude([
+ 'var',
+ 'vendor',
+ ])
+;
+
+return (new PhpCsFixer\Config())
+ ->setRules([
+ '@Symfony' => true,
+ '@PSR12' => true,
+ '@PHP82Migration' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'ordered_imports' => ['sort_algorithm' => 'alpha'],
+ 'no_unused_imports' => true,
+ 'global_namespace_import' => [
+ 'import_classes' => true,
+ 'import_constants' => true,
+ 'import_functions' => true,
+ ],
+ 'blank_line_before_statement' => [
+ 'statements' => ['return', 'throw', 'try', 'if'],
+ ],
+ 'multiline_whitespace_before_semicolons' => [
+ 'strategy' => 'new_line_for_chained_calls',
+ ],
+ 'class_attributes_separation' => [
+ 'elements' => [
+ 'const' => 'one',
+ 'method' => 'one',
+ 'property' => 'one',
+ 'trait_import' => 'none',
+ ],
+ ],
+ 'declare_strict_types' => true,
+ 'void_return' => true,
+ 'native_function_invocation' => [
+ 'include' => ['@all'],
+ ],
+ 'native_constant_invocation' => [
+ 'scope' => 'all',
+ ],
+ 'no_superfluous_phpdoc_tags' => [
+ 'allow_mixed' => true,
+ 'remove_inheritdoc' => true,
+ ],
+ 'phpdoc_align' => [
+ 'align' => 'left',
+ ],
+ 'phpdoc_order' => true,
+ 'phpdoc_separation' => true,
+ 'yoda_style' => false,
+ 'single_line_throw' => false,
+ 'trailing_comma_in_multiline' => [
+ 'elements' => ['arrays', 'arguments', 'parameters'],
+ ],
+ ])
+ ->setFinder($finder)
+ ->setCacheFile(__DIR__ . '/var/.php-cs-fixer.cache')
+ ->setRiskyAllowed(true)
+;
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 748ffd7..dc281ef 100644
--- a/composer.json
+++ b/composer.json
@@ -59,7 +59,8 @@
],
"post-update-cmd": [
"@auto-scripts"
- ]
+ ],
+ "fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --config=.php-cs-fixer.dist.php"
},
"conflict": {
"symfony/symfony": "*"
@@ -71,6 +72,7 @@
}
},
"require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.42",
"symfony/maker-bundle": "^1.62"
}
}
diff --git a/composer.lock b/composer.lock
index 79d9caf..20112b4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "7d2859d190dfcf0c235a4cf526f044af",
+ "content-hash": "b91cbe5e5e7804c9053c04c986e1b5cd",
"packages": [
{
"name": "doctrine/cache",
@@ -4329,6 +4329,507 @@
}
],
"packages-dev": [
+ {
+ "name": "clue/ndjson-react",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/clue/reactphp-ndjson.git",
+ "reference": "392dc165fce93b5bb5c637b67e59619223c931b0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0",
+ "reference": "392dc165fce93b5bb5c637b67e59619223c931b0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3",
+ "react/stream": "^1.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35",
+ "react/event-loop": "^1.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Clue\\React\\NDJson\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering"
+ }
+ ],
+ "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.",
+ "homepage": "https://github.com/clue/reactphp-ndjson",
+ "keywords": [
+ "NDJSON",
+ "json",
+ "jsonlines",
+ "newline",
+ "reactphp",
+ "streaming"
+ ],
+ "support": {
+ "issues": "https://github.com/clue/reactphp-ndjson/issues",
+ "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://clue.engineering/support",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/clue",
+ "type": "github"
+ }
+ ],
+ "time": "2022-12-23T10:58:28+00:00"
+ },
+ {
+ "name": "composer/pcre",
+ "version": "3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<1.11.10"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-strict-rules": "^1 || ^2",
+ "phpunit/phpunit": "^8 || ^9"
+ },
+ "type": "library",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-12T16:29:46+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "3.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
+ "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.11",
+ "symfony/phpunit-bridge": "^3 || ^7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/semver/issues",
+ "source": "https://github.com/composer/semver/tree/3.4.3"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-19T14:15:21+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "3.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1 || ^2 || ^3",
+ "php": "^7.2.5 || ^8.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/3.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-06T16:37:16+00:00"
+ },
+ {
+ "name": "evenement/evenement",
+ "version": "v3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/igorw/evenement.git",
+ "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc",
+ "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9 || ^6"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Evenement\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Igor Wiedler",
+ "email": "igor@wiedler.ch"
+ }
+ ],
+ "description": "Événement is a very simple event dispatching library for PHP",
+ "keywords": [
+ "event-dispatcher",
+ "event-emitter"
+ ],
+ "support": {
+ "issues": "https://github.com/igorw/evenement/issues",
+ "source": "https://github.com/igorw/evenement/tree/v3.0.2"
+ },
+ "time": "2023-08-08T05:53:35+00:00"
+ },
+ {
+ "name": "fidry/cpu-core-counter",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theofidry/cpu-core-counter.git",
+ "reference": "8520451a140d3f46ac33042715115e290cf5785f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f",
+ "reference": "8520451a140d3f46ac33042715115e290cf5785f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "fidry/makefile": "^0.2.0",
+ "fidry/php-cs-fixer-config": "^1.1.2",
+ "phpstan/extension-installer": "^1.2.0",
+ "phpstan/phpstan": "^1.9.2",
+ "phpstan/phpstan-deprecation-rules": "^1.0.0",
+ "phpstan/phpstan-phpunit": "^1.2.2",
+ "phpstan/phpstan-strict-rules": "^1.4.4",
+ "phpunit/phpunit": "^8.5.31 || ^9.5.26",
+ "webmozarts/strict-phpunit": "^7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Fidry\\CpuCoreCounter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Théo FIDRY",
+ "email": "theo.fidry@gmail.com"
+ }
+ ],
+ "description": "Tiny utility to get the number of CPU cores.",
+ "keywords": [
+ "CPU",
+ "core"
+ ],
+ "support": {
+ "issues": "https://github.com/theofidry/cpu-core-counter/issues",
+ "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theofidry",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-06T10:04:20+00:00"
+ },
+ {
+ "name": "friendsofphp/php-cs-fixer",
+ "version": "v3.72.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
+ "reference": "900389362c43d116fee1ffc51f7878145fa61b57"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/900389362c43d116fee1ffc51f7878145fa61b57",
+ "reference": "900389362c43d116fee1ffc51f7878145fa61b57",
+ "shasum": ""
+ },
+ "require": {
+ "clue/ndjson-react": "^1.0",
+ "composer/semver": "^3.4",
+ "composer/xdebug-handler": "^3.0.3",
+ "ext-filter": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "fidry/cpu-core-counter": "^1.2",
+ "php": "^7.4 || ^8.0",
+ "react/child-process": "^0.6.5",
+ "react/event-loop": "^1.0",
+ "react/promise": "^2.0 || ^3.0",
+ "react/socket": "^1.0",
+ "react/stream": "^1.0",
+ "sebastian/diff": "^4.0 || ^5.1 || ^6.0 || ^7.0",
+ "symfony/console": "^5.4 || ^6.4 || ^7.0",
+ "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0",
+ "symfony/filesystem": "^5.4 || ^6.4 || ^7.0",
+ "symfony/finder": "^5.4 || ^6.4 || ^7.0",
+ "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0",
+ "symfony/polyfill-mbstring": "^1.31",
+ "symfony/polyfill-php80": "^1.31",
+ "symfony/polyfill-php81": "^1.31",
+ "symfony/process": "^5.4 || ^6.4 || ^7.2",
+ "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0"
+ },
+ "require-dev": {
+ "facile-it/paraunit": "^1.3.1 || ^2.6",
+ "infection/infection": "^0.29.14",
+ "justinrainbow/json-schema": "^5.3 || ^6.2",
+ "keradus/cli-executor": "^2.1",
+ "mikey179/vfsstream": "^1.6.12",
+ "php-coveralls/php-coveralls": "^2.7",
+ "php-cs-fixer/accessible-object": "^1.1",
+ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
+ "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
+ "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.12",
+ "symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.3",
+ "symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.3"
+ },
+ "suggest": {
+ "ext-dom": "For handling output formats in XML",
+ "ext-mbstring": "For handling non-UTF8 characters."
+ },
+ "bin": [
+ "php-cs-fixer"
+ ],
+ "type": "application",
+ "autoload": {
+ "psr-4": {
+ "PhpCsFixer\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "src/Fixer/Internal/*"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Dariusz Rumiński",
+ "email": "dariusz.ruminski@gmail.com"
+ }
+ ],
+ "description": "A tool to automatically fix PHP code style",
+ "keywords": [
+ "Static code analysis",
+ "fixer",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
+ "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.72.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/keradus",
+ "type": "github"
+ }
+ ],
+ "time": "2025-03-13T11:25:37+00:00"
+ },
{
"name": "nikic/php-parser",
"version": "v5.4.0",
@@ -4387,6 +4888,599 @@
},
"time": "2024-12-30T11:07:19+00:00"
},
+ {
+ "name": "react/cache",
+ "version": "v1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/cache.git",
+ "reference": "d47c472b64aa5608225f47965a484b75c7817d5b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b",
+ "reference": "d47c472b64aa5608225f47965a484b75c7817d5b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "react/promise": "^3.0 || ^2.0 || ^1.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Async, Promise-based cache interface for ReactPHP",
+ "keywords": [
+ "cache",
+ "caching",
+ "promise",
+ "reactphp"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/cache/issues",
+ "source": "https://github.com/reactphp/cache/tree/v1.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2022-11-30T15:59:55+00:00"
+ },
+ {
+ "name": "react/child-process",
+ "version": "v0.6.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/child-process.git",
+ "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159",
+ "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159",
+ "shasum": ""
+ },
+ "require": {
+ "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
+ "php": ">=5.3.0",
+ "react/event-loop": "^1.2",
+ "react/stream": "^1.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
+ "react/socket": "^1.16",
+ "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\ChildProcess\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Event-driven library for executing child processes with ReactPHP.",
+ "keywords": [
+ "event-driven",
+ "process",
+ "reactphp"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/child-process/issues",
+ "source": "https://github.com/reactphp/child-process/tree/v0.6.6"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2025-01-01T16:37:48+00:00"
+ },
+ {
+ "name": "react/dns",
+ "version": "v1.13.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/dns.git",
+ "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5",
+ "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "react/cache": "^1.0 || ^0.6 || ^0.5",
+ "react/event-loop": "^1.2",
+ "react/promise": "^3.2 || ^2.7 || ^1.2.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
+ "react/async": "^4.3 || ^3 || ^2",
+ "react/promise-timer": "^1.11"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Dns\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Async DNS resolver for ReactPHP",
+ "keywords": [
+ "async",
+ "dns",
+ "dns-resolver",
+ "reactphp"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/dns/issues",
+ "source": "https://github.com/reactphp/dns/tree/v1.13.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-06-13T14:18:03+00:00"
+ },
+ {
+ "name": "react/event-loop",
+ "version": "v1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/event-loop.git",
+ "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
+ "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
+ },
+ "suggest": {
+ "ext-pcntl": "For signal handling support when using the StreamSelectLoop"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\EventLoop\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
+ "keywords": [
+ "asynchronous",
+ "event-loop"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/event-loop/issues",
+ "source": "https://github.com/reactphp/event-loop/tree/v1.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2023-11-13T13:48:05+00:00"
+ },
+ {
+ "name": "react/promise",
+ "version": "v3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/promise.git",
+ "reference": "8a164643313c71354582dc850b42b33fa12a4b63"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63",
+ "reference": "8a164643313c71354582dc850b42b33fa12a4b63",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "1.10.39 || 1.4.10",
+ "phpunit/phpunit": "^9.6 || ^7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "React\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "A lightweight implementation of CommonJS Promises/A for PHP",
+ "keywords": [
+ "promise",
+ "promises"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/promise/issues",
+ "source": "https://github.com/reactphp/promise/tree/v3.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-05-24T10:39:05+00:00"
+ },
+ {
+ "name": "react/socket",
+ "version": "v1.16.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/socket.git",
+ "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1",
+ "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1",
+ "shasum": ""
+ },
+ "require": {
+ "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
+ "php": ">=5.3.0",
+ "react/dns": "^1.13",
+ "react/event-loop": "^1.2",
+ "react/promise": "^3.2 || ^2.6 || ^1.2.1",
+ "react/stream": "^1.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
+ "react/async": "^4.3 || ^3.3 || ^2",
+ "react/promise-stream": "^1.4",
+ "react/promise-timer": "^1.11"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Socket\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
+ "keywords": [
+ "Connection",
+ "Socket",
+ "async",
+ "reactphp",
+ "stream"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/socket/issues",
+ "source": "https://github.com/reactphp/socket/tree/v1.16.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-07-26T10:38:09+00:00"
+ },
+ {
+ "name": "react/stream",
+ "version": "v1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/stream.git",
+ "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d",
+ "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d",
+ "shasum": ""
+ },
+ "require": {
+ "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
+ "php": ">=5.3.8",
+ "react/event-loop": "^1.2"
+ },
+ "require-dev": {
+ "clue/stream-filter": "~1.2",
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Stream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP",
+ "keywords": [
+ "event-driven",
+ "io",
+ "non-blocking",
+ "pipe",
+ "reactphp",
+ "readable",
+ "stream",
+ "writable"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/stream/issues",
+ "source": "https://github.com/reactphp/stream/tree/v1.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-06-11T12:45:25+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "7.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "7ab1ea946c012266ca32390913653d844ecd085f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f",
+ "reference": "7ab1ea946c012266ca32390913653d844ecd085f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0",
+ "symfony/process": "^7.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "security": "https://github.com/sebastianbergmann/diff/security/policy",
+ "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:55:46+00:00"
+ },
{
"name": "symfony/maker-bundle",
"version": "v1.62.1",
@@ -4479,6 +5573,73 @@
],
"time": "2025-01-15T00:21:40+00:00"
},
+ {
+ "name": "symfony/options-resolver",
+ "version": "v7.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/options-resolver.git",
+ "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50",
+ "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\OptionsResolver\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an improved replacement for the array_replace PHP function",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "config",
+ "configuration",
+ "options"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/options-resolver/tree/v7.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-20T11:17:29+00:00"
+ },
{
"name": "symfony/process",
"version": "v7.2.4",
diff --git a/config/services.yaml b/config/services.yaml
index 8247f30..d76d87d 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -24,16 +24,16 @@ services:
- '../src/Entity/'
- '../src/Kernel.php'
- # Calendar configuration services
- App\Core\Home\Calendar\CalendarConfig:
- factory: ['@App\Core\Home\Calendar\CalendarConfigFactory', 'createCalendarConfig']
-
- App\Core\Home\Calendar\CalendarConfigFactory:
+ App\Core\Services\Calendar\CalendarService:
+ arguments:
+ $providers:
+ - '@App\Services\Calendar\IcsCalendarProvider'
+
+ App\Services\Calendar\IcsCalendarProvider:
arguments:
$icsCalendars: '%app.calendars.ics%'
-
- App\Core\Home\Calendar\CalendarService:
- factory: ['@App\Core\Home\Calendar\CalendarFactory', 'createCalendarService']
+ $icsClient: '@App\Services\Calendar\IcsClient'
+ $icsParser: '@App\Services\Calendar\IcsParser'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
diff --git a/src/Command/ChatGPTCommand.php b/src/Command/ChatGPTCommand.php
index c85ec56..70d7bd5 100644
--- a/src/Command/ChatGPTCommand.php
+++ b/src/Command/ChatGPTCommand.php
@@ -1,8 +1,11 @@
getOption('system-prompt');
- $conversation = $this->chatGPTService->createChatConversation($systemPrompt ? [$systemPrompt] : []);
+ $conversation = $this->chatService->createChatConversation($systemPrompt ? [$systemPrompt] : []);
$io->info('Starting chat with ChatGPT (type "exit" to quit)');
while (true) {
$userMessage = $io->ask('You');
-
+
if ($userMessage === 'exit') {
return Command::SUCCESS;
}
try {
- $response = $this->chatGPTService->sendMessage($userMessage, $conversation);
- $this->chatGPTService->addMessageToConversation($conversation, $userMessage, 'user');
- $this->chatGPTService->addMessageToConversation($conversation, $response, 'assistant');
-
- $io->text(['ChatGPT > ' . $response, '']);
- } catch (\Exception $e) {
+ $response = $this->chatService->sendMessage($userMessage, $conversation);
+ $this->chatService->addMessageToConversation($conversation, $userMessage, 'user');
+ $this->chatService->addMessageToConversation($conversation, $response, 'assistant');
+
+ $io->text(['ChatGPT > '.$response, '']);
+ } catch (Exception $e) {
$io->error($e->getMessage());
+
return Command::FAILURE;
}
}
diff --git a/src/Command/HomeAssistantCommand.php b/src/Command/HomeAssistantCommand.php
index fbcf126..12707c7 100644
--- a/src/Command/HomeAssistantCommand.php
+++ b/src/Command/HomeAssistantCommand.php
@@ -4,7 +4,11 @@ declare(strict_types=1);
namespace App\Command;
-use App\Core\HomeAssistant\HomeAssistantService;
+use App\Core\Services\Home\HomeEntityInterface;
+use App\Core\Services\Home\HomeServiceInterface;
+
+use function array_map;
+
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -13,13 +17,13 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
- name: 'app:home-assistant',
- description: 'Interact with Home Assistant',
+ name: 'app:home',
+ description: 'Interact with the configured Home Service',
)]
final class HomeAssistantCommand extends Command
{
public function __construct(
- private readonly HomeAssistantService $homeAssistant
+ private readonly HomeServiceInterface $homeService,
) {
parent::__construct();
}
@@ -27,96 +31,46 @@ final class HomeAssistantCommand extends Command
protected function configure(): void
{
$this
- ->addOption('list-domains', null, InputOption::VALUE_NONE, 'List all available domains')
->addOption('list-entities', null, InputOption::VALUE_NONE, 'List all entities')
->addOption('domain', null, InputOption::VALUE_REQUIRED, 'Filter entities by domain')
->addOption('entity-id', null, InputOption::VALUE_REQUIRED, 'Entity ID to interact with')
- ->addOption('turn-on', null, InputOption::VALUE_NONE, 'Turn on the specified entity')
- ->addOption('turn-off', null, InputOption::VALUE_NONE, 'Turn off the specified entity');
+ ;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
- if ($input->getOption('list-domains')) {
- return $this->listDomains($io);
- }
-
if ($input->getOption('list-entities')) {
- return $this->listEntities($io, $input->getOption('domain'));
+ return $this->listEntities($io);
}
$entityId = $input->getOption('entity-id');
+
if ($entityId === null) {
$io->error('You must specify an entity ID using --entity-id option');
+
return Command::FAILURE;
}
- if ($input->getOption('turn-on')) {
- return $this->turnOn($io, $entityId);
- }
-
- if ($input->getOption('turn-off')) {
- return $this->turnOff($io, $entityId);
- }
-
- $this->showEntityState($io, $entityId);
return Command::SUCCESS;
}
- private function listDomains(SymfonyStyle $io): int
+ private function listEntities(SymfonyStyle $io): int
{
- $domains = $this->homeAssistant->getAvailableDomains();
- $io->listing($domains);
- return Command::SUCCESS;
- }
-
- private function listEntities(SymfonyStyle $io, string|null $domain): int
- {
- $entities = $domain !== null
- ? $this->homeAssistant->getEntitiesByDomain($domain)
- : $this->homeAssistant->getAllEntityStates();
+ $entities = $this->homeService->findAllEntities();
$rows = array_map(
- static fn($entity) => [
- $entity->entityId,
+ static fn (HomeEntityInterface $entity) => [
+ $entity->getId(),
$entity->getName(),
- $entity->state,
+ $entity->getState(),
],
- $entities
+ $entities,
);
$io->table(['Entity ID', 'Name', 'State'], $rows);
- return Command::SUCCESS;
- }
- private function turnOn(SymfonyStyle $io, string $entityId): int
- {
- $state = $this->homeAssistant->turnOn($entityId);
- $io->success(sprintf('Entity %s turned on. Current state: %s', $entityId, $state->state));
return Command::SUCCESS;
}
-
- private function turnOff(SymfonyStyle $io, string $entityId): int
- {
- $state = $this->homeAssistant->turnOff($entityId);
- $io->success(sprintf('Entity %s turned off. Current state: %s', $entityId, $state->state));
- return Command::SUCCESS;
- }
-
- private function showEntityState(SymfonyStyle $io, string $entityId): void
- {
- $state = $this->homeAssistant->getEntityState($entityId);
- $io->table(
- ['Property', 'Value'],
- [
- ['Entity ID', $state->entityId],
- ['Name', $state->getName()],
- ['State', $state->state],
- ['Last Changed', $state->lastChanged],
- ['Last Updated', $state->lastUpdated],
- ]
- );
- }
}
diff --git a/src/Command/ReadCalendarCommand.php b/src/Command/ReadCalendarCommand.php
index 913d88f..e36b162 100644
--- a/src/Command/ReadCalendarCommand.php
+++ b/src/Command/ReadCalendarCommand.php
@@ -1,12 +1,14 @@
addOption('days', 'd', InputOption::VALUE_OPTIONAL, 'Number of days to look ahead', 7)
- ->addOption('group', 'g', InputOption::VALUE_NONE, 'Group events by calendar');
- }
-
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
-
- $days = (int)$input->getOption('days');
- $group = $input->getOption('group');
-
- $from = new \DateTime();
- $to = (new \DateTime())->modify("+$days days");
-
- $calendarService = $this->calendarFactory->createCalendarService();
-
- if ($group) {
- $events = $calendarService->getEventsGroupedByCalendar($from, $to);
-
- foreach ($events as $calendarName => $calendarEvents) {
- $io->section($calendarName);
- $this->displayEvents($io, $calendarEvents);
- }
-
- return Command::SUCCESS;
+
+ $days = 7;
+
+ $from = new DateTime();
+ $to = (new DateTime())->modify("+$days days");
+
+ $calendars = $this->calendarService->getCalendars();
+
+ foreach ($calendars as $calendar) {
+ $events = $calendar->getEvents($from, $to);
+ $io->section($calendar->getName());
+ $this->displayEvents($io, $events);
}
-
- $events = $calendarService->getEvents($from, $to);
- $this->displayEvents($io, $events);
-
+
return Command::SUCCESS;
}
-
+
private function displayEvents(SymfonyStyle $io, array $events): void
{
$rows = [];
@@ -67,13 +53,13 @@ class ReadCalendarCommand extends Command
$event->getEnd()->format('Y-m-d H:i'),
$event->getTitle(),
$event->getLocation(),
- $event->isAllDay() ? 'Yes' : 'No'
+ $event->isAllDay() ? 'Yes' : 'No',
];
}
-
+
$io->table(
['Start', 'End', 'Title', 'Location', 'All Day'],
- $rows
+ $rows,
);
}
}
diff --git a/src/Command/RunAgentCommand.php b/src/Command/RunAgentCommand.php
index 836497d..abf9ea5 100644
--- a/src/Command/RunAgentCommand.php
+++ b/src/Command/RunAgentCommand.php
@@ -1,8 +1,11 @@
agent->run();
$output->writeln($result['prompt']);
$output->writeln($result['response']);
-
+
return Command::SUCCESS;
- } catch (\Exception $e) {
- $output->writeln('' . $e->getMessage() . '');
-
+ } catch (Exception $e) {
+ $output->writeln(''.$e->getMessage().'');
+
return Command::FAILURE;
}
}
diff --git a/src/Controller/ChatController.php b/src/Controller/ChatController.php
index 8f98720..058c2f9 100644
--- a/src/Controller/ChatController.php
+++ b/src/Controller/ChatController.php
@@ -1,8 +1,14 @@
getContent(), true);
-
+
if (!isset($data['message'])) {
return $this->json(['error' => 'Missing message parameter'], Response::HTTP_BAD_REQUEST);
}
-
+
$previousMessages = $data['conversation'] ?? [];
-
+
try {
$response = $this->chatGPTService->sendMessage($data['message'], $previousMessages);
-
- // Add user message and AI response to the conversation history
+
if (empty($previousMessages)) {
$conversation = $this->chatGPTService->createChatConversation();
} else {
$conversation = $previousMessages;
}
-
+
$this->chatGPTService->addMessageToConversation($conversation, $data['message'], 'user');
$this->chatGPTService->addMessageToConversation($conversation, $response, 'assistant');
-
+
return $this->json([
'response' => $response,
- 'conversation' => $conversation
+ 'conversation' => $conversation,
]);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
return $this->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Controller/WebController.php b/src/Controller/WebController.php
index 2b6f50e..054abcf 100644
--- a/src/Controller/WebController.php
+++ b/src/Controller/WebController.php
@@ -1,5 +1,7 @@
render('chat/index.html.twig');
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/Agent/Agent.php b/src/Core/Agent/Agent.php
index 0738a47..5a94cd1 100644
--- a/src/Core/Agent/Agent.php
+++ b/src/Core/Agent/Agent.php
@@ -1,18 +1,22 @@
$prompt,
- 'response' => $response
+ 'response' => $response,
];
}
@@ -33,22 +37,20 @@ class Agent
$from = $now->modify('-1 day');
$to = $now->modify('+7 days');
- $events = $this->calendarService->getEvents($from, $to);
+ $calendars = $this->calendarService->getCalendars();
$calendarEventsText = '';
-
- foreach ($events as $event) {
+
+ foreach ($calendars as $calendar) {
$calendarEventsText .= sprintf(
- "- %s: %s from %s to %s\n",
- $event->getCalendarName(),
- $event->getTitle(),
- $event->getStart()->format('Y-m-d H:i'),
- $event->getEnd()->format('Y-m-d H:i')
+ "- %s: %s\n",
+ $calendar->getName(),
+ $calendar->getDescription(),
);
}
return strtr($this->promptProvider->getPromptTemplate(), [
'{calendar_events}' => $calendarEventsText,
- '{current_time}' => $now->format('Y-m-d H:i:s')
+ '{current_time}' => $now->format('Y-m-d H:i:s'),
]);
}
}
diff --git a/src/Core/Agent/PromptProvider.php b/src/Core/Agent/PromptProvider.php
index 5cfeb1a..3354c92 100644
--- a/src/Core/Agent/PromptProvider.php
+++ b/src/Core/Agent/PromptProvider.php
@@ -1,5 +1,7 @@
*/
- private array $icsCalendars = [];
-
- public function addIcsCalendar(string $name, string $url): self
- {
- $this->icsCalendars[$name] = $url;
-
- return $this;
- }
-
- /**
- * @return array
- */
- public function getIcsCalendars(): array
- {
- return $this->icsCalendars;
- }
-
- public function getIcsCalendarUrl(string $name): ?string
- {
- return $this->icsCalendars[$name] ?? null;
- }
-
- public function hasIcsCalendar(string $name): bool
- {
- return isset($this->icsCalendars[$name]);
- }
-}
\ No newline at end of file
diff --git a/src/Core/Home/Calendar/CalendarConfigFactory.php b/src/Core/Home/Calendar/CalendarConfigFactory.php
deleted file mode 100644
index 8e4aa1c..0000000
--- a/src/Core/Home/Calendar/CalendarConfigFactory.php
+++ /dev/null
@@ -1,25 +0,0 @@
- $icsCalendars
- */
- public function __construct(
- private readonly array $icsCalendars = []
- ) {
- }
-
- public function createCalendarConfig(): CalendarConfig
- {
- $config = new CalendarConfig();
-
- foreach ($this->icsCalendars as $name => $url) {
- $config->addIcsCalendar($name, $url);
- }
-
- return $config;
- }
-}
\ No newline at end of file
diff --git a/src/Core/Home/Calendar/CalendarEvent.php b/src/Core/Home/Calendar/CalendarEvent.php
deleted file mode 100644
index a58c3e2..0000000
--- a/src/Core/Home/Calendar/CalendarEvent.php
+++ /dev/null
@@ -1,64 +0,0 @@
-id;
- }
-
- public function getTitle(): string
- {
- return $this->title;
- }
-
- public function getStart(): \DateTimeInterface
- {
- return $this->start;
- }
-
- public function getEnd(): \DateTimeInterface
- {
- return $this->end;
- }
-
- public function getDescription(): string
- {
- return $this->description;
- }
-
- public function getLocation(): string
- {
- return $this->location;
- }
-
- public function getCalendarName(): string
- {
- return $this->calendarName;
- }
-
- public function getAttendees(): array
- {
- return $this->attendees;
- }
-
- public function isAllDay(): bool
- {
- return $this->allDay;
- }
-}
\ No newline at end of file
diff --git a/src/Core/Home/Calendar/CalendarFactory.php b/src/Core/Home/Calendar/CalendarFactory.php
deleted file mode 100644
index 56c41d4..0000000
--- a/src/Core/Home/Calendar/CalendarFactory.php
+++ /dev/null
@@ -1,25 +0,0 @@
-httpClient);
-
- foreach ($this->config->getIcsCalendars() as $name => $url) {
- $service->addIcsCalendar($url, $name);
- }
-
- return $service;
- }
-}
\ No newline at end of file
diff --git a/src/Core/Home/Calendar/CalendarInterface.php b/src/Core/Home/Calendar/CalendarInterface.php
deleted file mode 100644
index 36c4951..0000000
--- a/src/Core/Home/Calendar/CalendarInterface.php
+++ /dev/null
@@ -1,19 +0,0 @@
-calendarProviders[] = $calendar;
-
- return $this;
- }
-
- public function addIcsCalendar(string $url, ?string $name = null): self
- {
- $provider = new IcsCalendarProvider($this->httpClient, $url, $name);
- $this->calendarProviders[] = $provider;
-
- return $this;
- }
-
- public function getCalendars(): array
- {
- return $this->calendarProviders;
- }
-
- public function getEvents(\DateTimeInterface $from, \DateTimeInterface $to): array
- {
- $allEvents = [];
-
- foreach ($this->calendarProviders as $calendar) {
- $events = $calendar->getEvents($from, $to);
- $allEvents = array_merge($allEvents, $events);
- }
-
- // Sort events by start date
- usort($allEvents, function (CalendarEvent $a, CalendarEvent $b) {
- return $a->getStart() <=> $b->getStart();
- });
-
- return $allEvents;
- }
-
- public function getEventsGroupedByCalendar(\DateTimeInterface $from, \DateTimeInterface $to): array
- {
- $groupedEvents = [];
-
- foreach ($this->calendarProviders as $calendar) {
- $calendarName = $calendar->getName();
- $events = $calendar->getEvents($from, $to);
-
- if (!empty($events)) {
- $groupedEvents[$calendarName] = $events;
- }
- }
-
- return $groupedEvents;
- }
-}
\ No newline at end of file
diff --git a/src/Core/Home/Calendar/IcsCalendarProvider.php b/src/Core/Home/Calendar/IcsCalendarProvider.php
deleted file mode 100644
index e8ad66d..0000000
--- a/src/Core/Home/Calendar/IcsCalendarProvider.php
+++ /dev/null
@@ -1,136 +0,0 @@
-url = $url;
- $this->name = $name ?? parse_url($url, PHP_URL_HOST) ?? 'Unknown';
- }
-
- public function getName(): string
- {
- return $this->name;
- }
-
- public function getEvents(\DateTimeInterface $from, \DateTimeInterface $to): array
- {
- $icsContent = $this->fetchIcsContent();
-
- return $this->parseIcsContent($icsContent, $from, $to);
- }
-
- private function fetchIcsContent(): string
- {
- // Cache for 5 minutes
- if ($this->cachedContent !== null && $this->lastFetch !== null &&
- $this->lastFetch->getTimestamp() > (time() - 300)) {
- return $this->cachedContent;
- }
-
- $requestUrl = $this->url;
-
- // Convert webcal:// to https:// for the HTTP client
- if (stripos($requestUrl, 'webcal://') === 0) {
- $requestUrl = str_replace('webcal://', 'https://', $requestUrl);
- }
-
- $response = $this->httpClient->request('GET', $requestUrl);
- $content = $response->getContent();
-
- $this->cachedContent = $content;
- $this->lastFetch = new \DateTime();
-
- return $content;
- }
-
- private function parseIcsContent(string $icsContent, \DateTimeInterface $from, \DateTimeInterface $to): array
- {
- $events = [];
- $lines = explode("\n", $icsContent);
-
- $inEvent = false;
- $currentEvent = null;
- $eventData = [];
-
- foreach ($lines as $line) {
- $line = trim($line);
-
- if ($line === 'BEGIN:VEVENT') {
- $inEvent = true;
- $eventData = [];
- continue;
- }
-
- if ($line === 'END:VEVENT') {
- $inEvent = false;
-
- if (isset($eventData['DTSTART'], $eventData['DTEND'], $eventData['UID'])) {
- $startDate = $this->parseIcsDate($eventData['DTSTART']);
- $endDate = $this->parseIcsDate($eventData['DTEND']);
-
- // Skip events outside the requested range
- if ($endDate < $from || $startDate > $to) {
- continue;
- }
-
- $allDay = false;
- if (isset($eventData['DTSTART;VALUE=DATE'])) {
- $allDay = true;
- }
-
- $events[] = new CalendarEvent(
- $eventData['UID'],
- $eventData['SUMMARY'] ?? 'Untitled Event',
- $startDate,
- $endDate,
- $eventData['DESCRIPTION'] ?? '',
- $eventData['LOCATION'] ?? '',
- $this->name,
- [], // attendees not parsed in this basic implementation
- $allDay
- );
- }
-
- continue;
- }
-
- if ($inEvent && strpos($line, ':') !== false) {
- [$key, $value] = explode(':', $line, 2);
-
- // Handle property parameters
- if (strpos($key, ';') !== false) {
- $parts = explode(';', $key);
- $key = $parts[0];
- }
-
- $eventData[$key] = $value;
- }
- }
-
- return $events;
- }
-
- private function parseIcsDate(string $dateString): \DateTimeInterface
- {
- $date = new DateTimeImmutable($dateString);
- if ($date === false) {
- return new \DateTime();
- }
-
- return $date;
- }
-}
\ No newline at end of file
diff --git a/src/Core/HomeAssistant/HomeAssistantService.php b/src/Core/HomeAssistant/HomeAssistantService.php
deleted file mode 100644
index 6a19818..0000000
--- a/src/Core/HomeAssistant/HomeAssistantService.php
+++ /dev/null
@@ -1,75 +0,0 @@
-client->getStates();
-
- return array_map(
- static fn (array $state): EntityState => EntityState::fromArray($state),
- $states
- );
- }
-
- public function getEntityState(string $entityId): EntityState
- {
- $state = $this->client->getEntityState($entityId);
-
- return EntityState::fromArray($state);
- }
-
- /**
- * @return EntityState[]
- */
- public function getEntitiesByDomain(string $domain): array
- {
- $allStates = $this->getAllEntityStates();
-
- return array_filter(
- $allStates,
- static fn (EntityState $state): bool => $state->getDomain() === $domain
- );
- }
-
- public function turnOn(string $entityId): EntityState
- {
- $result = $this->client->turnOn($entityId);
-
- return $this->getEntityState($entityId);
- }
-
- public function turnOff(string $entityId): EntityState
- {
- $result = $this->client->turnOff($entityId);
-
- return $this->getEntityState($entityId);
- }
-
- public function callService(string $domain, string $service, array $data = []): array
- {
- return $this->client->callService($domain, $service, $data);
- }
-
- /**
- * @return string[]
- */
- public function getAvailableDomains(): array
- {
- $services = $this->client->getServices();
-
- return array_keys($services);
- }
-}
\ No newline at end of file
diff --git a/src/Core/Services/AI/ChatServiceInterface.php b/src/Core/Services/AI/ChatServiceInterface.php
new file mode 100644
index 0000000..21f34d6
--- /dev/null
+++ b/src/Core/Services/AI/ChatServiceInterface.php
@@ -0,0 +1,14 @@
+name;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
+ public function isEnabled(): bool
+ {
+ return $this->enabled;
+ }
+
+ public function isDefault(): bool
+ {
+ return $this->isDefault;
+ }
+
+ /**
+ * @return CalendarEvent[]
+ */
+ public function getEvents(): array
+ {
+ return $this->events;
+ }
+
+ public function addEvent(CalendarEvent $event): void
+ {
+ $this->events[] = $event;
+ }
+
+ /**
+ * @param CalendarEvent[] $events
+ */
+ public function setEvents(array $events): void
+ {
+ $this->events = $events;
+ }
+}
diff --git a/src/Core/Services/Calendar/CalendarEvent.php b/src/Core/Services/Calendar/CalendarEvent.php
new file mode 100644
index 0000000..9c62fb1
--- /dev/null
+++ b/src/Core/Services/Calendar/CalendarEvent.php
@@ -0,0 +1,62 @@
+title;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function getStart(): DateTimeInterface
+ {
+ return $this->start;
+ }
+
+ public function getEnd(): DateTimeInterface
+ {
+ return $this->end;
+ }
+
+ public function getLocation(): ?string
+ {
+ return $this->location;
+ }
+
+ public function getUrl(): ?string
+ {
+ return $this->url;
+ }
+
+ public function getCalendar(): Calendar
+ {
+ return $this->calendar;
+ }
+
+ public function isAllDay(): bool
+ {
+ return $this->allDay;
+ }
+}
diff --git a/src/Core/Services/Calendar/CalendarProviderInterface.php b/src/Core/Services/Calendar/CalendarProviderInterface.php
new file mode 100644
index 0000000..e480a2b
--- /dev/null
+++ b/src/Core/Services/Calendar/CalendarProviderInterface.php
@@ -0,0 +1,13 @@
+ $providers
+ */
+ public function __construct(
+ private iterable $providers,
+ ) {
+ }
+
+ public function getCalendars(): array
+ {
+ $allCalendars = [];
+
+ foreach ($this->providers as $provider) {
+ $calendars = $provider->getCalendars();
+ $allCalendars = array_merge($allCalendars, $calendars);
+ }
+
+ usort($allCalendars, function (Calendar $a, Calendar $b) {
+ return $a->getName() <=> $b->getName();
+ });
+
+ return $allCalendars;
+ }
+}
diff --git a/src/Core/Services/Home/HomeEntityInterface.php b/src/Core/Services/Home/HomeEntityInterface.php
new file mode 100644
index 0000000..0cb9b5b
--- /dev/null
+++ b/src/Core/Services/Home/HomeEntityInterface.php
@@ -0,0 +1,22 @@
+
+ */
+ public function findAllEntities(): array;
+
+ public function callService(string $service, array $data = []): array;
+}
diff --git a/src/Kernel.php b/src/Kernel.php
index 779cd1f..ad0fb48 100644
--- a/src/Kernel.php
+++ b/src/Kernel.php
@@ -1,5 +1,7 @@
$icsCalendars
+ */
+ public function __construct(
+ private readonly array $icsCalendars,
+ private readonly IcsClient $icsClient,
+ private readonly IcsParser $icsParser,
+ ) {
+ }
+
+ /**
+ * @return Calendar[]
+ */
+ public function getCalendars(): array
+ {
+ $calendars = [];
+
+ foreach ($this->icsCalendars as $name => $url) {
+ $calendar = new Calendar(
+ name: $name,
+ description: sprintf('ICS Calendar: %s', $name),
+ url: $url,
+ enabled: true,
+ isDefault: false,
+ );
+
+ $content = $this->icsClient->fetchCalendarContent($url);
+ $this->icsParser->parseEvents($calendar, $content);
+
+ $calendars[] = $calendar;
+ }
+
+ return $calendars;
+ }
+}
diff --git a/src/Services/Calendar/IcsClient.php b/src/Services/Calendar/IcsClient.php
new file mode 100644
index 0000000..9f4a090
--- /dev/null
+++ b/src/Services/Calendar/IcsClient.php
@@ -0,0 +1,37 @@
+normalizeUrl($url);
+
+ $response = $this->httpClient->request('GET', $requestUrl);
+
+ return $response->getContent();
+ }
+
+ private function normalizeUrl(string $url): string
+ {
+ if (str_starts_with(strtolower($url), 'webcal://')) {
+ return str_replace('webcal://', 'https://', $url);
+ }
+
+ return $url;
+ }
+}
diff --git a/src/Services/Calendar/IcsParser.php b/src/Services/Calendar/IcsParser.php
new file mode 100644
index 0000000..7bc1054
--- /dev/null
+++ b/src/Services/Calendar/IcsParser.php
@@ -0,0 +1,119 @@
+parseIcsDate($eventData['DTSTART']);
+
+ // If DTEND is not present, use DTSTART as end date
+ $endDate = isset($eventData['DTEND'])
+ ? $this->parseIcsDate($eventData['DTEND'])
+ : $startDate;
+
+ $event = new \App\Core\Services\Calendar\CalendarEvent(
+ $eventData['SUMMARY'],
+ $startDate,
+ $endDate,
+ $calendar,
+ $eventData['DESCRIPTION'] ?? null,
+ $eventData['LOCATION'] ?? null,
+ $eventData['URL'] ?? null,
+ $eventData['ALLDAY'] ?? false,
+ );
+
+ $events[] = $event;
+ continue;
+ }
+
+ if ($inEvent && str_contains($line, ':')) {
+ // Handle line folding according to RFC 5545
+ if (str_starts_with($line, ' ') || str_starts_with($line, "\t")) {
+ if (!empty($eventData)) {
+ $keys = array_keys($eventData);
+ $lastKey = end($keys);
+ $eventData[$lastKey] .= substr($line, 1);
+ }
+ continue;
+ }
+
+ [$key, $value] = explode(':', $line, 2);
+
+ // Handle parameters like DTSTART;TZID=Europe/Berlin
+ if (str_contains($key, ';')) {
+ $parts = explode(';', $key);
+ $key = $parts[0];
+ }
+
+ $eventData[$key] = $value;
+ }
+ }
+
+ usort($events, fn ($a, $b) => $a->getStart() <=> $b->getStart());
+
+ foreach ($events as $event) {
+ $calendar->addEvent($event);
+ }
+ }
+
+ private function parseIcsDate(string $dateString): DateTimeImmutable
+ {
+ // Try various date formats that can appear in ICS files
+ $formats = [
+ 'Ymd\THis\Z', // UTC format
+ 'Ymd\THis', // Local time format
+ 'Ymd', // Date-only format
+ ];
+
+ foreach ($formats as $format) {
+ $date = DateTimeImmutable::createFromFormat($format, $dateString);
+
+ if ($date !== false) {
+ return $date;
+ }
+ }
+
+ // If all parsing attempts fail, return current time
+ return new DateTimeImmutable();
+ }
+}
diff --git a/src/Core/HomeAssistant/EntityState.php b/src/Services/HomeAssistant/EntityState.php
similarity index 85%
rename from src/Core/HomeAssistant/EntityState.php
rename to src/Services/HomeAssistant/EntityState.php
index 26ef1c7..d80821c 100644
--- a/src/Core/HomeAssistant/EntityState.php
+++ b/src/Services/HomeAssistant/EntityState.php
@@ -2,7 +2,10 @@
declare(strict_types=1);
-namespace App\Core\HomeAssistant;
+namespace App\Services\HomeAssistant;
+
+use function explode;
+use function in_array;
final readonly class EntityState
{
@@ -12,10 +15,10 @@ final readonly class EntityState
public array $attributes,
public string $lastChanged,
public string $lastUpdated,
- public array|string|null $context = null
+ public array|string|null $context = null,
) {
}
-
+
public static function fromArray(array $data): self
{
return new self(
@@ -24,28 +27,29 @@ final readonly class EntityState
$data['attributes'] ?? [],
$data['last_changed'] ?? '',
$data['last_updated'] ?? '',
- $data['context'] ?? null
+ $data['context'] ?? null,
);
}
-
+
public function isOn(): bool
{
return in_array($this->state, ['on', 'home', 'open', 'unlocked', 'active'], true);
}
-
+
public function isOff(): bool
{
return in_array($this->state, ['off', 'away', 'closed', 'locked', 'inactive'], true);
}
-
+
public function getDomain(): string
{
$parts = explode('.', $this->entityId, 2);
+
return $parts[0];
}
-
+
public function getName(): string
{
return $this->attributes['friendly_name'] ?? $this->entityId;
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/HomeAssistant/HomeAssistantClient.php b/src/Services/HomeAssistant/HomeAssistantClient.php
similarity index 91%
rename from src/Core/HomeAssistant/HomeAssistantClient.php
rename to src/Services/HomeAssistant/HomeAssistantClient.php
index 297b425..dc8923e 100644
--- a/src/Core/HomeAssistant/HomeAssistantClient.php
+++ b/src/Services/HomeAssistant/HomeAssistantClient.php
@@ -2,11 +2,14 @@
declare(strict_types=1);
-namespace App\Core\HomeAssistant;
+namespace App\Services\HomeAssistant;
+use function explode;
+
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
-use Symfony\Component\DependencyInjection\Attribute\Autowire;
+
final class HomeAssistantClient
{
public function __construct(
@@ -16,7 +19,7 @@ final class HomeAssistantClient
#[Autowire('%env(HOME_ASSISTANT_TOKEN)%')]
private readonly string $token,
#[Autowire('%env(HOME_ASSISTANT_VERIFY_SSL)%')]
- private readonly bool $verifySSL
+ private readonly bool $verifySSL,
) {
}
@@ -24,34 +27,36 @@ final class HomeAssistantClient
{
return $this->request('GET', '/api/states');
}
-
+
public function getServices(): array
{
return $this->request('GET', '/api/services');
}
-
+
public function getEntityState(string $entityId): array
{
return $this->request('GET', "/api/states/{$entityId}");
}
-
+
public function callService(string $domain, string $service, array $data = []): array
{
return $this->request('POST', "/api/services/{$domain}/{$service}", $data);
}
-
+
public function turnOn(string $entityId): array
{
$domain = explode('.', $entityId)[0];
+
return $this->callService($domain, 'turn_on', ['entity_id' => $entityId]);
}
-
+
public function turnOff(string $entityId): array
{
$domain = explode('.', $entityId)[0];
+
return $this->callService($domain, 'turn_off', ['entity_id' => $entityId]);
}
-
+
private function request(string $method, string $endpoint, array $data = []): array
{
$options = [
@@ -62,29 +67,30 @@ final class HomeAssistantClient
'verify_peer' => $this->verifySSL,
'verify_host' => $this->verifySSL,
];
-
+
if (!empty($data)) {
$options['json'] = $data;
}
-
+
$response = $this->httpClient->request(
$method,
- $this->baseUrl . $endpoint,
- $options
+ $this->baseUrl.$endpoint,
+ $options,
);
-
+
return $this->handleResponse($response);
}
-
+
private function handleResponse(ResponseInterface $response): array
{
$statusCode = $response->getStatusCode();
-
+
if ($statusCode >= 200 && $statusCode < 300) {
return $response->toArray();
}
-
+
$content = $response->getContent(false);
+
throw new HomeAssistantException($content, $statusCode);
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/HomeAssistant/HomeAssistantEntity.php b/src/Services/HomeAssistant/HomeAssistantEntity.php
new file mode 100644
index 0000000..62e42b9
--- /dev/null
+++ b/src/Services/HomeAssistant/HomeAssistantEntity.php
@@ -0,0 +1,67 @@
+entityState->entityId;
+ }
+
+ public function getState(): mixed
+ {
+ return $this->entityState->state;
+ }
+
+ public function getName(): string
+ {
+ return $this->entityState->getName();
+ }
+
+ public function getType(): HomeEntityType
+ {
+ $domain = $this->entityState->getDomain();
+
+ return match ($domain) {
+ 'light' => HomeEntityType::LIGHT,
+ 'switch' => HomeEntityType::SWITCH,
+ 'sensor' => HomeEntityType::SENSOR,
+ 'binary_sensor' => HomeEntityType::BINARY_SENSOR,
+ 'climate' => HomeEntityType::CLIMATE,
+ 'media_player' => HomeEntityType::MEDIA_PLAYER,
+ 'scene' => HomeEntityType::SCENE,
+ 'script' => HomeEntityType::SCRIPT,
+ 'automation' => HomeEntityType::AUTOMATION,
+ 'camera' => HomeEntityType::CAMERA,
+ 'cover' => HomeEntityType::COVER,
+ 'fan' => HomeEntityType::FAN,
+ 'lock' => HomeEntityType::LOCK,
+ 'vacuum' => HomeEntityType::VACUUM,
+ 'weather' => HomeEntityType::WEATHER,
+ 'zone' => HomeEntityType::ZONE,
+ default => throw new HomeAssistantException("Unknown entity type: {$domain}")
+ };
+ }
+
+ public function getLastChanged(): DateTimeImmutable
+ {
+ return new DateTimeImmutable($this->entityState->lastChanged);
+ }
+
+ public function getLastUpdated(): DateTimeImmutable
+ {
+ return new DateTimeImmutable($this->entityState->lastUpdated);
+ }
+}
diff --git a/src/Core/HomeAssistant/HomeAssistantException.php b/src/Services/HomeAssistant/HomeAssistantException.php
similarity index 74%
rename from src/Core/HomeAssistant/HomeAssistantException.php
rename to src/Services/HomeAssistant/HomeAssistantException.php
index a9d0784..b70e75a 100644
--- a/src/Core/HomeAssistant/HomeAssistantException.php
+++ b/src/Services/HomeAssistant/HomeAssistantException.php
@@ -2,10 +2,10 @@
declare(strict_types=1);
-namespace App\Core\HomeAssistant;
+namespace App\Services\HomeAssistant;
use RuntimeException;
final class HomeAssistantException extends RuntimeException
{
-}
\ No newline at end of file
+}
diff --git a/src/Services/HomeAssistant/HomeAssistantHomeService.php b/src/Services/HomeAssistant/HomeAssistantHomeService.php
new file mode 100644
index 0000000..d8ec898
--- /dev/null
+++ b/src/Services/HomeAssistant/HomeAssistantHomeService.php
@@ -0,0 +1,112 @@
+getEntityState($entityId);
+
+ return new HomeAssistantEntity($entityState);
+ }
+
+ public function findAllEntities(): array
+ {
+ $states = $this->getAllEntityStates();
+
+ if (empty($states)) {
+ throw new HomeAssistantException('No entities found');
+ }
+
+ return array_map(
+ fn (EntityState $state) => new HomeAssistantEntity($state),
+ $states,
+ );
+ }
+
+ public function callService(string $service, array $data = []): array
+ {
+ // Extract domain and service name from the service string
+ if (str_contains($service, '.')) {
+ [$domain, $serviceName] = explode('.', $service, 2);
+
+ return $this->client->callService($domain, $serviceName, $data);
+ }
+
+ throw new HomeAssistantException("Invalid service format. Expected 'domain.service'");
+ }
+
+ /**
+ * @return EntityState[]
+ */
+ public function getAllEntityStates(): array
+ {
+ $states = $this->client->getStates();
+
+ return array_map(
+ static fn (array $state): EntityState => EntityState::fromArray($state),
+ $states,
+ );
+ }
+
+ public function getEntityState(string $entityId): EntityState
+ {
+ $state = $this->client->getEntityState($entityId);
+
+ return EntityState::fromArray($state);
+ }
+
+ /**
+ * @return EntityState[]
+ */
+ public function getEntitiesByDomain(string $domain): array
+ {
+ $allStates = $this->getAllEntityStates();
+
+ return array_filter(
+ $allStates,
+ static fn (EntityState $state): bool => $state->getDomain() === $domain,
+ );
+ }
+
+ public function turnOn(string $entityId): EntityState
+ {
+ $this->client->turnOn($entityId);
+
+ return $this->getEntityState($entityId);
+ }
+
+ public function turnOff(string $entityId): EntityState
+ {
+ $this->client->turnOff($entityId);
+
+ return $this->getEntityState($entityId);
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getAvailableDomains(): array
+ {
+ $services = $this->client->getServices();
+
+ return array_keys($services);
+ }
+}
diff --git a/src/Core/OpenAI/ChatGPTService.php b/src/Services/OpenAI/ChatGPTService.php
similarity index 81%
rename from src/Core/OpenAI/ChatGPTService.php
rename to src/Services/OpenAI/ChatGPTService.php
index 3170488..7fa11a1 100644
--- a/src/Core/OpenAI/ChatGPTService.php
+++ b/src/Services/OpenAI/ChatGPTService.php
@@ -1,13 +1,17 @@
openAIClient->chat($messages);
-
+
if (!isset($response['choices'][0]['message']['content'])) {
throw new BadRequestException('Invalid response from OpenAI API');
}
return $response['choices'][0]['message']['content'];
- } catch (\Exception $e) {
- throw new BadRequestException('Error communicating with OpenAI API: ' . $e->getMessage());
+ } catch (Exception $e) {
+ throw new BadRequestException('Error communicating with OpenAI API: '.$e->getMessage());
}
}
public function createChatConversation(array $systemPrompt = []): array
{
$conversation = [];
-
+
if (!empty($systemPrompt)) {
$conversation[] = [
'role' => 'system',
'content' => $systemPrompt,
];
}
-
+
return $conversation;
}
-
+
public function addMessageToConversation(array &$conversation, string $message, string $role = 'user'): void
{
$conversation[] = [
@@ -53,4 +57,4 @@ class ChatGPTService
'content' => $message,
];
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/OpenAI/OpenAIClient.php b/src/Services/OpenAI/OpenAIClient.php
similarity index 95%
rename from src/Core/OpenAI/OpenAIClient.php
rename to src/Services/OpenAI/OpenAIClient.php
index 7ecd37a..9cbf5c5 100644
--- a/src/Core/OpenAI/OpenAIClient.php
+++ b/src/Services/OpenAI/OpenAIClient.php
@@ -1,6 +1,8 @@
$data,
]);
}
-}
\ No newline at end of file
+}
diff --git a/symfony.lock b/symfony.lock
index 76b1f9c..999ff18 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -26,6 +26,18 @@
"migrations/.gitignore"
]
},
+ "friendsofphp/php-cs-fixer": {
+ "version": "3.72",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.0",
+ "ref": "be2103eb4a20942e28a6dd87736669b757132435"
+ },
+ "files": [
+ ".php-cs-fixer.dist.php"
+ ]
+ },
"symfony/console": {
"version": "7.2",
"recipe": {
diff --git a/tests/Core/Home/Calendar/CalendarServiceTest.php b/tests/Core/Home/Calendar/CalendarServiceTest.php
index 01b7bdf..7e4b1ed 100644
--- a/tests/Core/Home/Calendar/CalendarServiceTest.php
+++ b/tests/Core/Home/Calendar/CalendarServiceTest.php
@@ -1,24 +1,31 @@
httpClient = $this->createMock(HttpClientInterface::class);
$this->calendarService = new CalendarService($this->httpClient);
}
-
+
public function testGetEventsFromMultipleCalendars(): void
{
// Create mock calendar providers
@@ -28,53 +35,53 @@ class CalendarServiceTest extends TestCase
$this->createEvent('Event 1', '2023-01-01 10:00', '2023-01-01 11:00', 'Calendar 1'),
$this->createEvent('Event 2', '2023-01-02 15:00', '2023-01-02 16:00', 'Calendar 1'),
]);
-
+
$calendar2 = $this->createMock(CalendarInterface::class);
$calendar2->method('getName')->willReturn('Calendar 2');
$calendar2->method('getEvents')->willReturn([
$this->createEvent('Event 3', '2023-01-01 12:00', '2023-01-01 13:00', 'Calendar 2'),
$this->createEvent('Event 4', '2023-01-03 09:00', '2023-01-03 10:00', 'Calendar 2'),
]);
-
+
// Add calendar providers to service
$this->calendarService->addCalendar($calendar1);
$this->calendarService->addCalendar($calendar2);
-
+
// Test getting all events
- $from = new \DateTime('2023-01-01');
- $to = new \DateTime('2023-01-03');
-
+ $from = new DateTime('2023-01-01');
+ $to = new DateTime('2023-01-03');
+
$events = $this->calendarService->getEvents($from, $to);
-
+
// Assertions
$this->assertCount(4, $events);
-
+
// Check if events are sorted by start date
$this->assertEquals('Event 1', $events[0]->getTitle());
$this->assertEquals('Event 3', $events[1]->getTitle());
$this->assertEquals('Event 2', $events[2]->getTitle());
$this->assertEquals('Event 4', $events[3]->getTitle());
-
+
// Test getting events grouped by calendar
$groupedEvents = $this->calendarService->getEventsGroupedByCalendar($from, $to);
-
+
$this->assertCount(2, $groupedEvents);
$this->assertArrayHasKey('Calendar 1', $groupedEvents);
$this->assertArrayHasKey('Calendar 2', $groupedEvents);
$this->assertCount(2, $groupedEvents['Calendar 1']);
$this->assertCount(2, $groupedEvents['Calendar 2']);
}
-
+
private function createEvent(string $title, string $start, string $end, string $calendarName): CalendarEvent
{
return new CalendarEvent(
- md5($title . $start),
+ md5($title.$start),
$title,
- new \DateTime($start),
- new \DateTime($end),
+ new DateTime($start),
+ new DateTime($end),
'Description',
'Location',
- $calendarName
+ $calendarName,
);
}
-}
\ No newline at end of file
+}
diff --git a/tests/Services/HomeAssistant/HomeAssistantEntityTest.php b/tests/Services/HomeAssistant/HomeAssistantEntityTest.php
new file mode 100644
index 0000000..a8e5c8f
--- /dev/null
+++ b/tests/Services/HomeAssistant/HomeAssistantEntityTest.php
@@ -0,0 +1,126 @@
+ 'Living Room Light'],
+ '2023-03-14T12:00:00+00:00',
+ '2023-03-14T12:00:00+00:00',
+ );
+
+ $entity = new HomeAssistantEntity($entityState);
+
+ $this->assertInstanceOf(HomeEntityInterface::class, $entity);
+ }
+
+ public function testGetId(): void
+ {
+ $entityState = new EntityState(
+ 'light.living_room',
+ 'on',
+ ['friendly_name' => 'Living Room Light'],
+ '2023-03-14T12:00:00+00:00',
+ '2023-03-14T12:00:00+00:00',
+ );
+
+ $entity = new HomeAssistantEntity($entityState);
+
+ $this->assertEquals('light.living_room', $entity->getId());
+ }
+
+ public function testGetState(): void
+ {
+ $entityState = new EntityState(
+ 'light.living_room',
+ 'on',
+ ['friendly_name' => 'Living Room Light'],
+ '2023-03-14T12:00:00+00:00',
+ '2023-03-14T12:00:00+00:00',
+ );
+
+ $entity = new HomeAssistantEntity($entityState);
+
+ $this->assertEquals('on', $entity->getState());
+ }
+
+ public function testGetName(): void
+ {
+ $entityState = new EntityState(
+ 'light.living_room',
+ 'on',
+ ['friendly_name' => 'Living Room Light'],
+ '2023-03-14T12:00:00+00:00',
+ '2023-03-14T12:00:00+00:00',
+ );
+
+ $entity = new HomeAssistantEntity($entityState);
+
+ $this->assertEquals('Living Room Light', $entity->getName());
+ }
+
+ public function testGetType(): void
+ {
+ $entityState = new EntityState(
+ 'light.living_room',
+ 'on',
+ ['friendly_name' => 'Living Room Light'],
+ '2023-03-14T12:00:00+00:00',
+ '2023-03-14T12:00:00+00:00',
+ );
+
+ $entity = new HomeAssistantEntity($entityState);
+
+ $this->assertEquals(HomeEntityType::LIGHT, $entity->getType());
+ }
+
+ public function testGetLastChanged(): void
+ {
+ $timestamp = '2023-03-14T12:00:00+00:00';
+ $expectedDateTime = new DateTimeImmutable($timestamp);
+
+ $entityState = new EntityState(
+ 'light.living_room',
+ 'on',
+ ['friendly_name' => 'Living Room Light'],
+ $timestamp,
+ '2023-03-14T12:30:00+00:00',
+ );
+
+ $entity = new HomeAssistantEntity($entityState);
+
+ $this->assertEquals($expectedDateTime, $entity->getLastChanged());
+ }
+
+ public function testGetLastUpdated(): void
+ {
+ $timestamp = '2023-03-14T12:30:00+00:00';
+ $expectedDateTime = new DateTimeImmutable($timestamp);
+
+ $entityState = new EntityState(
+ 'light.living_room',
+ 'on',
+ ['friendly_name' => 'Living Room Light'],
+ '2023-03-14T12:00:00+00:00',
+ $timestamp,
+ );
+
+ $entity = new HomeAssistantEntity($entityState);
+
+ $this->assertEquals($expectedDateTime, $entity->getLastUpdated());
+ }
+}
diff --git a/tests/Services/HomeAssistant/HomeAssistantHomeServiceTest.php b/tests/Services/HomeAssistant/HomeAssistantHomeServiceTest.php
new file mode 100644
index 0000000..e572dc4
--- /dev/null
+++ b/tests/Services/HomeAssistant/HomeAssistantHomeServiceTest.php
@@ -0,0 +1,108 @@
+homeAssistantClient = $this->createMock(HomeAssistantClient::class);
+ $this->homeService = new HomeAssistantHomeService($this->homeAssistantClient);
+ }
+
+ public function testFindEntity(): void
+ {
+ // Create expected raw entity state response
+ $rawEntityState = [
+ 'entity_id' => 'light.living_room',
+ 'state' => 'on',
+ 'attributes' => ['friendly_name' => 'Living Room Light'],
+ 'last_changed' => '2023-03-14T12:00:00+00:00',
+ 'last_updated' => '2023-03-14T12:00:00+00:00',
+ ];
+
+ // Configure the mock to return our test entity state
+ $this->homeAssistantClient->expects($this->once())
+ ->method('getEntityState')
+ ->with('light.living_room')
+ ->willReturn($rawEntityState)
+ ;
+
+ // Call the method under test
+ $entity = $this->homeService->findEntity('light.living_room');
+
+ // Assert the result is a HomeEntityInterface
+ $this->assertInstanceOf(HomeEntityInterface::class, $entity);
+ $this->assertInstanceOf(HomeAssistantEntity::class, $entity);
+
+ // Assert the properties match
+ $this->assertEquals('light.living_room', $entity->getId());
+ $this->assertEquals('on', $entity->getState());
+ $this->assertEquals('Living Room Light', $entity->getName());
+ }
+
+ public function testFindAllEntities(): void
+ {
+ // Create expected raw entity state response
+ $rawEntityState = [
+ 'entity_id' => 'light.living_room',
+ 'state' => 'on',
+ 'attributes' => ['friendly_name' => 'Living Room Light'],
+ 'last_changed' => '2023-03-14T12:00:00+00:00',
+ 'last_updated' => '2023-03-14T12:00:00+00:00',
+ ];
+
+ // Configure the mock to return our test entity states
+ $this->homeAssistantClient->expects($this->once())
+ ->method('getStates')
+ ->willReturn([$rawEntityState])
+ ;
+
+ // Call the method under test
+ $entities = $this->homeService->findAllEntities();
+
+ // Verify result is an array with one entity
+ $this->assertIsArray($entities);
+ $this->assertCount(1, $entities);
+ $entity = $entities[0];
+
+ // Assert the result is a HomeEntityInterface
+ $this->assertInstanceOf(HomeEntityInterface::class, $entity);
+ $this->assertInstanceOf(HomeAssistantEntity::class, $entity);
+
+ // Assert it returns the first entity
+ $this->assertEquals('light.living_room', $entity->getId());
+ }
+
+ public function testCallService(): void
+ {
+ // Expected result from HomeAssistant
+ $expectedResult = ['success' => true];
+
+ // Configure the mock to return our test result
+ $this->homeAssistantClient->expects($this->once())
+ ->method('callService')
+ ->with('light', 'turn_on', ['entity_id' => 'light.living_room'])
+ ->willReturn($expectedResult)
+ ;
+
+ // Call the method under test
+ $result = $this->homeService->callService('light.turn_on', ['entity_id' => 'light.living_room']);
+
+ // Assert the result is as expected
+ $this->assertEquals($expectedResult, $result);
+ }
+}