t3chw1zard / emporia-connect
A framework-agnostic PHP client for the Emporia Vue energy monitoring API, with Laravel and Symfony integration.
Requires
- php: ^8.2
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- guzzlehttp/psr7: ^2.6
- phpseclib/phpseclib: ^3.0.36
- psr/cache: ^2.0 || ^3.0
- psr/clock: ^1.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/simple-cache: ^2.0 || ^3.0
Requires (Dev)
- illuminate/config: ^11.0 || ^12.0
- illuminate/container: ^11.0 || ^12.0
- illuminate/support: ^11.0 || ^12.0
- laravel/pint: ^1.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
- rector/rector: ^2.0
- symfony/cache: ^6.4 || ^7.0
- symfony/config: ^6.4 || ^7.0
- symfony/dependency-injection: ^6.4 || ^7.0
- symfony/http-kernel: ^6.4 || ^7.0
- vlucas/phpdotenv: ^5.6
Suggests
- ext-gmp: Speeds up the Cognito SRP login handshake considerably.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-30 07:20:46 UTC
README
A framework-agnostic PHP 8.2+ client for the Emporia Energy Vue API: energy monitors, smart plugs and EV chargers.
It covers the full API: devices, usage, outlets, EV chargers and vehicles. It comes with typed, immutable response objects, token caching, and first-class Laravel and Symfony integration.
Features
- Devices, location properties, device status, channels, channel types, instant usage (with nested smart plugs), chart usage, outlets, EV chargers, vehicles and the maintenance check
- AWS Cognito login with SRP (like the Emporia app) or plain password auth
- Token caching in any PSR-16 or PSR-6 cache (or a JSON file). Tokens are refreshed with the refresh token, so the client logs in with your password only when it has to
- Automatic token refresh on expiry or a
401, and retries with exponential back-off on5xx - Typed, immutable,
JsonSerializableresponse classes - PSR-18 HTTP client (Guzzle by default), PSR-17 factories, PSR-20 clock
- Laravel service provider (auto-discovered), config file and
Emporiafacade withEmporia::fake() - Symfony bundle with configuration tree
FakeClientfor your own tests- PHPStan level 9
Installation
composer require t3chw1zard/emporia-connect
Installing ext-gmp is recommended: it makes the SRP login handshake much faster.
Quick start
use T3chW1zard\EmporiaConnect\EmporiaConnect; use T3chW1zard\EmporiaConnect\Enums\Scale; $client = EmporiaConnect::client('you@example.com', 'your-password', cache: $psr16OrPsr6Cache); $customer = $client->customers()->me(); $devices = $client->devices()->all(); // Current usage of every channel on a device (kWh used during the last minute) $usage = $client->usage()->devices($devices[0]->deviceGid, scale: Scale::MINUTE); foreach ($usage[$devices[0]->deviceGid]->channels as $channel) { printf("%s: %.0f W\n", $channel->name, $channel->watts()); }
Creating a client never touches the network. Tokens are fetched from the cache, refreshed, or obtained by logging in on the first API call.
Authentication and token caching
The Emporia API authenticates with AWS Cognito tokens. The id token is valid for one hour; the refresh token for 30 days. Pass a cache and the client stores the token set there, so later requests (and later PHP processes) reuse or refresh it instead of logging in again.
// PSR-16 (Laravel cache repository, Symfony Psr16Cache, ...) or PSR-6 (Symfony cache pools, ...) $client = EmporiaConnect::client('you@example.com', 'secret', cache: $cache); // A JSON token file $client = EmporiaConnect::builder() ->withCredentials('you@example.com', 'secret') ->withTokenFile(__DIR__.'/storage/emporia-tokens.json') ->build(); // Existing tokens, no password $client = EmporiaConnect::fromTokens($idToken, $accessToken, $refreshToken, cache: $cache); // Your own storage, e.g. a database table $client = EmporiaConnect::client('you@example.com', 'secret', cache: new MyTokenStore()); // implements TokenStoreContract
The cache entry contains a refresh token, so treat the cache as sensitive storage.
Full configuration
use T3chW1zard\EmporiaConnect\Enums\AuthFlow; use T3chW1zard\EmporiaConnect\Http\RetryPolicy; $client = EmporiaConnect::builder() ->withCredentials('you@example.com', 'secret') ->withCache($cache, key: 'emporia.tokens', ttl: 2592000) ->withAuthFlow(AuthFlow::SRP) // or AuthFlow::PASSWORD ->withTimeouts(connectTimeout: 6.03, readTimeout: 10.03) ->withRetryPolicy(new RetryPolicy(maxAttempts: 5, initialDelayMs: 500, maxDelayMs: 30000)) ->withTokenRefreshLeeway(60) // refresh this many seconds before expiry ->withHttpClient($psr18Client) // optional, Guzzle by default ->build();
API reference
| Method | Description |
|---|---|
EmporiaConnect::client() / fromTokens() / builder() |
Create a client |
$client->downForMaintenance() |
Maintenance message, or null when the API is up |
$client->customers()->me() |
The logged in customer |
$client->devices()->all() / find($gid) |
Devices, including sub-devices |
$client->devices()->locationProperties($gid) / populateLocationProperties($device) |
Location and billing settings |
$client->devices()->status() / withConnectionStatus($devices) |
Outlets, chargers and online status |
$client->channels()->all($gid) / find($gid, $channelNum) |
Channels of a device |
$client->channels()->update($channel->withName('Oven')) |
Rename or re-type a channel |
$client->channels()->types() |
Available channel types |
$client->usage()->devices($gids, $instant, $scale, $unit) |
Instant usage of one or more devices |
$client->usage()->chart($gid, $channelNum, $start, $end, $scale, $unit) / chartForChannel($channel, ...) |
Usage over a time range |
$client->outlets()->all() / update($outlet, on: true) / turnOn() / turnOff() |
Smart plugs |
$client->chargers()->all() / update($charger, on: true, chargeRate: 32) / turnOn() / turnOff() |
EV chargers |
$client->vehicles()->all() / status($gid) |
Linked vehicles |
Devices
$devices = $client->devices()->all(); // list<DeviceResponse>; sub-devices are included in the list $device = $devices[0]; $device->deviceGid; // 12345 $device->model; // "VUE002" $device->channels; // list<DeviceChannelResponse> $device->outlet; // ?OutletResponse (smart plugs) $device->evCharger; // ?ChargerResponse (EV chargers) $device->locationProperties?->timeZone; // "America/New_York" $device->name(); // display name from the app $status = $client->devices()->status(); // outlets, chargers and devicesConnected $devices = $client->devices()->withConnectionStatus($devices);
Usage
usage values are the energy used during one scale period in the requested unit (kWh by default).
watts() converts kWh to average power.
use T3chW1zard\EmporiaConnect\Enums\Scale; use T3chW1zard\EmporiaConnect\Enums\Unit; // Instant usage of one or more devices, including nested smart plugs / sub-panels $usage = $client->usage()->devices([12345, 67890], scale: Scale::MINUTE, unit: Unit::KILOWATT_HOURS); $mains = $usage[12345]->channel('1,2,3'); $mains->usage; // 0.05 (kWh in the last minute) $mains->watts(); // 3000.0 $mains->percentage; // 100.0 $mains->nestedDevices; // array<int, UsageDeviceResponse> // Historical usage of one channel $chart = $client->usage()->chart(12345, '1,2,3', start: '-1 day', scale: Scale::HOUR); $chart->usage; // list<float|null> $chart->points(); // list<array{time: DateTimeImmutable, usage: float|null}> $chart->watts(); $chart->total();
Channel 1,2,3 is the mains and Balance is the unmonitored remainder.
usage()->devices() retries (with back-off) while the API still returns null for recent data.
Outlets and chargers
$outlet = $client->outlets()->all()[0]; $client->outlets()->turnOff($outlet); $charger = $client->chargers()->all()[0]; $client->chargers()->update($charger, on: true, chargeRate: 32); // amps
Channels
$channel = $client->channels()->find(12345, '1'); $client->channels()->update($channel->withName('Kitchen')->withChannelMultiplier(2.0)); $types = $client->channels()->types(); // list<ChannelTypeResponse>
Vehicles
$vehicle = $client->vehicles()->all()[0]; $status = $client->vehicles()->status($vehicle->vehicleGid); $status?->batteryLevel;
Responses
All responses are final readonly classes implementing ResponseContract (toArray() and
JsonSerializable), so you can return them straight from a controller:
return response()->json($client->devices()->all()); // Laravel return new JsonResponse($client->devices()->all()); // Symfony
Errors
All exceptions extend T3chW1zard\EmporiaConnect\Exceptions\EmporiaException:
| Exception | When |
|---|---|
AuthenticationException |
Cognito login/refresh failed (wrong password, MFA required, ...) |
ApiException |
The API answered with an error status ($e->statusCode, $e->responseBody) |
TransportException |
The request could not be sent (DNS, connection, timeout) |
Laravel
The service provider and the Emporia facade are auto-discovered. Add your credentials to .env:
EMPORIA_USERNAME=you@example.com EMPORIA_PASSWORD=your-password
Tokens are cached in your default cache store. Publish the config to change this:
php artisan vendor:publish --tag=emporia-config
Inject the contract, or use the facade:
use T3chW1zard\EmporiaConnect\Contracts\ClientContract; use T3chW1zard\EmporiaConnect\Laravel\Facades\Emporia; public function index(ClientContract $emporia) { return $emporia->devices()->all(); } Emporia::usage()->devices(12345);
See docs/laravel.md for configuration and testing.
Symfony
Register the bundle and configure it:
// config/bundles.php return [ T3chW1zard\EmporiaConnect\Symfony\EmporiaConnectBundle::class => ['all' => true], ];
# config/packages/emporia_connect.yaml emporia_connect: username: '%env(EMPORIA_USERNAME)%' password: '%env(EMPORIA_PASSWORD)%' cache: cache.app
Then autowire T3chW1zard\EmporiaConnect\Contracts\ClientContract. See docs/symfony.md.
Testing your application
Type-hint ClientContract and swap in the FakeClient, which returns realistic fixture data and
records every request:
use T3chW1zard\EmporiaConnect\Testing\FakeClient; $fake = new FakeClient([ 'GET customers' => ['customerGid' => 1, 'email' => 'test@example.com'], 'GET AppAPI?apiMethod=getChartUsage*' => ['firstUsageInstant' => '2024-01-01T00:00:00Z', 'usageList' => [1.0, 2.0]], ]); $fake->outlets()->turnOn($fake->outlets()->all()[0]); $fake->transporter()->hasSent('PUT', 'devices/outlet'); // true
Development
composer test # PHPUnit composer analyse # PHPStan level 9 composer format # Laravel Pint composer qa # everything
License
MIT