PHPackages                             graystackit/laravel-testo-api - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. graystackit/laravel-testo-api

ActiveLibrary[API Development](/categories/api)

graystackit/laravel-testo-api
=============================

Laravel package for the Testo Saveris Data API, built on Saloon 4

02PHP

Since Apr 8Pushed 2mo agoCompare

[ Source](https://github.com/GraystackIT/laravel-testo-api)[ Packagist](https://packagist.org/packages/graystackit/laravel-testo-api)[ RSS](/packages/graystackit-laravel-testo-api/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependenciesVersions (2)Used By (0)

graystackit/laravel-testo-cloud
===============================

[](#graystackitlaravel-testo-cloud)

A Laravel package for the **Testo Smart Connect API**, built on [Saloon 4](https://docs.saloon.dev/).

Retrieve historical measurements, alarm events, HACCP task records, device properties, device health status, measuring object configurations, and location hierarchies from Testo Smart Connect — all with a clean, Laravel-native interface.

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 11, 12, or 13

Installation
------------

[](#installation)

```
composer require graystackit/laravel-testo-api
```

Laravel auto-discovers the service provider. Then publish the config file:

```
php artisan vendor:publish --tag=testo-config
```

Configuration
-------------

[](#configuration)

Add the following to your `.env` file:

```
TESTO_API_KEY=your-api-key

# Optional — defaults shown
TESTO_REGION=eu                # eu | am | ap
TESTO_HTTP_TIMEOUT=30
TESTO_DOWNLOAD_TIMEOUT=120

# Store fetched measurements in the database (default: true)
TESTO_STORE_MEASUREMENTS=true

# Polling behaviour for testo:fetch-measurements
TESTO_POLL_INTERVAL=5          # seconds between status checks
TESTO_POLL_MAX_ATTEMPTS=60     # give up after this many checks (5 min total by default)
TESTO_DEFAULT_FROM_DAYS=7      # days to look back when --from is omitted
```

Generate your API key from the Smart Connect home page. Keys are valid for up to one year.

Async Workflow
--------------

[](#async-workflow)

All data-export endpoints use the same two-step async pattern:

1. **Submit** a POST request → receive a `request_uuid`
2. **Poll** a GET request with that UUID until status is `Completed`
3. **Download** each URL in `dataUrls` using `downloadDataFile()`

```
submit() → Submitted → Processing → Completed
                                 ↓
                               Failed

```

Usage
-----

[](#usage)

Resolve `TestoCloudClient` from the container or inject via constructor.

```
use GraystackIT\TestoCloud\TestoCloudClient;

$client = app(TestoCloudClient::class);
```

---

### Measurements `POST /v2/measurements` • `GET /v2/measurements/{uuid}`

[](#measurements--post-v2measurements----get-v2measurementsuuid)

```
use Carbon\Carbon;

// 1. Submit
$submit = $client->submitMeasurementRequest(
    from:   Carbon::parse('2025-01-01'),
    to:     Carbon::parse('2025-01-31'),
    format: 'JSON',  // optional, default 'JSON'
);

echo $submit->requestUuid;

// 2. Poll
$status = $client->checkRequestStatus($submit->requestUuid);

if ($status->isCompleted()) {
    foreach ($status->dataUrls as $url) {
        $content = $client->downloadDataFile($url);
        // parse with MeasurementNdjsonParser...
    }
}
```

> `$from` must be strictly before `$to` — an `\InvalidArgumentException` is thrown otherwise.

#### Parse NDJSON measurement data

[](#parse-ndjson-measurement-data)

```
use GraystackIT\TestoCloud\Parsers\MeasurementNdjsonParser;

$measurements = (new MeasurementNdjsonParser())->parse($content);
// [['timestamp' => '...', 'temperature' => 21.5, 'humidity' => 55.0], ...]
```

---

### Alarms `POST /v3/alarms` • `GET /v3/alarms/{uuid}`

[](#alarms--post-v3alarms----get-v3alarmsuuid)

Retrieve historical alarm events for the configured account.

```
use Carbon\Carbon;

// 1. Submit
$submit = $client->submitAlarmRequest(
    from: Carbon::parse('2025-01-01'),
    to:   Carbon::parse('2025-01-31'),
);

// 2. Poll
$status = $client->checkAlarmStatus($submit->requestUuid);

if ($status->isCompleted()) {
    foreach ($status->dataUrls as $url) {
        $content = $client->downloadDataFile($url);
    }
}
```

---

### Tasks `POST /v3/tasks` • `GET /v3/tasks/{uuid}`

[](#tasks--post-v3tasks----get-v3tasksuuid)

Retrieve quality-management / HACCP activity records executed by staff.

```
$submit = $client->submitTaskRequest(
    from: Carbon::parse('2025-01-01'),
    to:   Carbon::parse('2025-01-31'),
);

$status = $client->checkTaskStatus($submit->requestUuid);

if ($status->isFailed()) {
    logger()->error($status->error);
}
```

---

### Device Properties `POST /v3/devices/properties` • `GET /v3/devices/properties/{uuid}`

[](#device-properties--post-v3devicesproperties----get-v3devicespropertiesuuid)

Retrieve device metadata including serial numbers, model codes, firmware versions, calibration status, and equipment relationships.

> No date range required — returns current configuration.

```
$submit = $client->submitEquipmentRequest(format: 'JSON');  // format optional

$status = $client->checkEquipmentStatus($submit->requestUuid);

if ($status->isCompleted()) {
    foreach ($status->dataUrls as $url) {
        $content = $client->downloadDataFile($url);
    }
}
```

---

### Device Status `POST /v3/devices/status` • `GET /v3/devices/status/{uuid}`

[](#device-status--post-v3devicesstatus----get-v3devicesstatusuuid)

Retrieve battery level, signal strength, last communication timestamp, firmware version, and serial numbers for all devices.

> No date range required — returns current status snapshot.

```
$submit = $client->submitSensorStatusRequest();

$status = $client->checkSensorStatus($submit->requestUuid);

if ($status->isCompleted()) {
    $content = $client->downloadDataFile($status->dataUrls[0]);
}
```

---

### Measuring Objects `POST /v1/measuring-objects` • `GET /v1/measuring-objects/{uuid}`

[](#measuring-objects--post-v1measuring-objects----get-v1measuring-objectsuuid)

Retrieve measuring-object configurations including `customer_uuid`, `customer_site`, `product_family_id`, measurement settings, and channel assignments.

> No date range required — returns current configuration.

```
$submit = $client->submitMeasuringObjectRequest();

$status = $client->checkMeasuringObjectStatus($submit->requestUuid);
```

---

### Locations `POST /v1/locations` • `GET /v1/locations/{uuid}`

[](#locations--post-v1locations----get-v1locationsuuid)

Retrieve the location hierarchy (sites, zones, rooms) associated with the account.

> No date range required — returns current location structure.

```
$submit = $client->submitLocationRequest(format: 'JSON');  // format optional

$status = $client->checkLocationStatus($submit->requestUuid);

if ($status->isCompleted()) {
    foreach ($status->dataUrls as $url) {
        $content = $client->downloadDataFile($url);
    }

    // Optional metadata file
    if ($status->metadataUrl) {
        $meta = $client->downloadDataFile($status->metadataUrl);
    }
}
```

---

Data Objects
------------

[](#data-objects)

### Submission response — `AsyncSubmitResponse`

[](#submission-response--asyncsubmitresponse)

Returned by every `submit*()` method.

PropertyTypeDescription`requestUuid``string`UUID to use when polling status`status``AsyncRequestStatus`Enum — `Submitted` on initial response### Status response — `AsyncStatusResponse`

[](#status-response--asyncstatusresponse)

Returned by every `check*Status()` method.

PropertyTypeDescription`status``AsyncRequestStatus`Current state (see enum below)`dataUrls``string[]`Download URLs (populated when completed)`metadataUrl``?string`Metadata file URL (populated when completed)`error``?string`Error message (populated when failed)Helper methods: `isCompleted()`, `isProcessing()`, `isFailed()`

### `AsyncRequestStatus` enum

[](#asyncrequeststatus-enum)

```
use GraystackIT\TestoCloud\Enums\AsyncRequestStatus;

AsyncRequestStatus::Submitted   // initial acknowledgment
AsyncRequestStatus::Processing  // API is preparing data ("In Progress" normalised)
AsyncRequestStatus::Completed   // data ready for download
AsyncRequestStatus::Failed      // see $response->error
```

The enum normalises all API status strings, including `"In Progress"` → `Processing`.

### Legacy measurement objects

[](#legacy-measurement-objects)

Used by `checkRequestStatus()` (measurements only):

ClassProperties`MeasurementSubmitResponse``requestUuid`, `status` (string)`MeasurementStatusResponse``status`, `dataUrls[]`, `metadataUrl`, `error`, helpers---

Error Handling
--------------

[](#error-handling)

All client methods throw `GraystackIT\TestoCloud\Exceptions\TestoApiException` on failure.

```
use GraystackIT\TestoCloud\Exceptions\TestoApiException;

try {
    $submit = $client->submitAlarmRequest(Carbon::parse('2025-01-01'), Carbon::parse('2025-02-01'));
} catch (TestoApiException $e) {
    // HTTP errors, unexpected API responses
    logger()->error($e->getMessage(), ['code' => $e->getCode()]);
}
```

`submit*Request()` methods that accept a date range additionally throw `\InvalidArgumentException` when `$from >= $to`.

If `TESTO_API_KEY` is not configured, a `\RuntimeException` is thrown on container resolution.

---

API Endpoint Reference
----------------------

[](#api-endpoint-reference)

ModuleSubmitCheck StatusMeasurements`POST /v2/measurements``GET /v2/measurements/{uuid}`Alarms`POST /v3/alarms``GET /v3/alarms/{uuid}`Tasks`POST /v3/tasks``GET /v3/tasks/{uuid}`Device Properties`POST /v3/devices/properties``GET /v3/devices/properties/{uuid}`Device Status`POST /v3/devices/status``GET /v3/devices/status/{uuid}`Measuring Objects`POST /v1/measuring-objects``GET /v1/measuring-objects/{uuid}`Locations`POST /v1/locations``GET /v1/locations/{uuid}`Base URL: `https://data-api.{region}.smartconnect.testo.com`

---

Database Storage
----------------

[](#database-storage)

### Migration

[](#migration)

Run the migration to create the `testo_measurements` table:

```
php artisan migrate
```

To customise the migration before running it, publish it first:

```
php artisan vendor:publish --tag=testo-migrations
```

#### `testo_measurements` schema

[](#testo_measurements-schema)

ColumnTypeDescription`id`bigintAuto-increment primary key`logger_uuid`string|nullLogger device UUID (populated when available)`measured_at`timestampTimestamp of the measurement`temperature`decimal(8,4)|nullTemperature reading`humidity`decimal(8,4)|nullHumidity reading`created_at`timestampRecord insertion time`updated_at`timestampRecord update time### Model

[](#model)

```
use GraystackIT\TestoCloud\Models\TestoMeasurement;

$recent = TestoMeasurement::where('measured_at', '>=', now()->subDay())->get();

foreach ($recent as $row) {
    echo $row->measured_at;   // Carbon instance
    echo $row->temperature;   // float|null
    echo $row->humidity;      // float|null
}
```

### Disabling automatic storage

[](#disabling-automatic-storage)

Set `TESTO_STORE_MEASUREMENTS=false` in your `.env` (or `store_measurements => false` in `config/testo.php`) to fetch and parse data without writing to the database.

---

Artisan Command
---------------

[](#artisan-command)

```
php artisan testo:fetch-measurements
```

Fetches historical measurements from the Testo API, parses the NDJSON response, and — when storage is enabled — persists every row to `testo_measurements`.

### Options

[](#options)

OptionDefaultDescription`--from=``default_from_days` agoStart date (`Y-m-d`)`--to=`todayEnd date (`Y-m-d`)`--format=``JSON`Export format (`JSON` or `CSV`)`--logger-uuid=`*(all loggers)*Filter results to a single device UUID### Examples

[](#examples)

```
# Last 7 days (default)
php artisan testo:fetch-measurements

# Specific date range
php artisan testo:fetch-measurements --from=2025-01-01 --to=2025-01-31

# Single device only
php artisan testo:fetch-measurements --from=2025-03-01 --to=2025-03-31 --logger-uuid=abc-123
```

### Console output

[](#console-output)

```
Fetching measurements from 2025-01-01 to 2025-01-31...
Request submitted. UUID: 9f4a1b2c-...
Polling for completion (max 60 attempts, 5s interval)...
  [1/60] Status: Submitted — waiting 5s...
  [2/60] Status: Processing — waiting 5s...
Status: completed.
Downloading 2 data file(s)...
  [1/2] Downloaded.
  [2/2] Downloaded.
Parsed 2880 measurement(s) across 2 file(s).

Total measurements parsed: 2880
Stored in database: 2880

```

---

Testing
-------

[](#testing)

```
composer test
```

Tests use Saloon's `MockClient` — no real API calls are made.

License
-------

[](#license)

MIT

###  Health Score

21

—

LowBetter than 17% of packages

Maintenance57

Moderate activity, may be stable

Popularity3

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity14

Early-stage or recently created project

 Bus Factor1

Top contributor holds 90.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/ee65c1290d44c83f1c44c965c311a0150c50b4a8eae9e94635973a8a5d245d55?d=identicon)[bharb](/maintainers/bharb)

---

Top Contributors

[![Parvesh96](https://avatars.githubusercontent.com/u/96613912?v=4)](https://github.com/Parvesh96 "Parvesh96 (10 commits)")[![bharb82](https://avatars.githubusercontent.com/u/159435438?v=4)](https://github.com/bharb82 "bharb82 (1 commits)")

### Embed Badge

![Health badge](/badges/graystackit-laravel-testo-api/health.svg)

```
[![Health](https://phpackages.com/badges/graystackit-laravel-testo-api/health.svg)](https://phpackages.com/packages/graystackit-laravel-testo-api)
```

###  Alternatives

[exsyst/swagger

A php library to manipulate Swagger specifications

35916.4M7](/packages/exsyst-swagger)[hubspot/api-client

Hubspot API client

24016.2M20](/packages/hubspot-api-client)[pocketmine/bedrock-protocol

An implementation of the Minecraft: Bedrock Edition protocol in PHP

172445.0k18](/packages/pocketmine-bedrock-protocol)[botman/driver-telegram

Telegram driver for BotMan

93459.5k6](/packages/botman-driver-telegram)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
