Skip to content

Ecozen Integration Guide

This guide explains how to connect Ecozen Ecofrost machines to Coldtivate. It covers:

  • System administrators: setup, configuration, rollout and troubleshooting.
  • Developers: how the integration works in the Base API and the mobile app.

For how sensors work in general, see Integrations & Data Collection. All identifiers and values below are synthetic examples.


1. How the integration works

Ecozen machines report their readings to Ecozen's cloud. Coldtivate never connects to the machines themselves. It uses two Ecozen interfaces, each for one job:

Interface Used for Credentials
Ecozen Cloud (cloud.ecozen.ai, signing in through openid.ecozen.ai) Finding which machines a user's Ecozen Cloud account can see. Used only while a sensor is being set up. The user's own Cloud username and password, typed in the app. Used for one request and never stored.
Ecozen v3 API (apis.ecozen.ai) Checking the selected machine and reading its temperature, humidity and set point. Coldtivate's OAuth2 client (ECOZEN_CLIENT_ID, ECOZEN_CLIENT_SECRET), set on the server. Never sent to the app.
Step What happens
1 Ecozen enables each machine for Coldtivate's OAuth client.
2 A Registered Employee chooses Ecozen in the app and signs in with their Ecozen Cloud account.
3 The Base API signs in to Ecozen Cloud and returns the account's machines. It makes no v3 API calls yet.
4 The user picks a machine and presses Test connection. The Base API asks the v3 API for that machine's recent temperature readings, using Coldtivate's OAuth client.
5 The user saves the cooling unit. The save verifies the signed selection ticket and company permissions without contacting Ecozen, and stores the machine ID with an empty password.
6 After the save commits, the Base API queues the first poll. From then on it polls every 4 hours through the v3 API. It never uses Ecozen Cloud for this.
7 The readings appear in the app under Cooling Units → Room Conditions.

Key facts:

  • No typed machine IDs. The user selects a machine from their Cloud account. There's no machine-ID field and no chamber selection: one machine is one cooling unit.
  • Cloud credentials are transient. They're used for one discovery request and are never saved, logged or sent back to the app.
  • A machine must pass both checks. Seeing it in the Cloud account doesn't mean Coldtivate's client can read it. The connection test checks that separately. If access is rejected, the app tells the user to contact Coldtivate support.
  • OAuth only. All Ecozen data requests use Coldtivate's OAuth client. There's no fallback to customer credentials or to the Cloud portal.
  • Readings: room temperature (°C) and relative humidity (%), with the set point attached to temperature readings.
  • History: when a machine is linked, the last 24 hours are imported. Older history isn't imported.

2. Requirements

Machines and accounts

Item Why
The customer has an Ecozen Cloud account that lists their machines, with a plain username and password sign-in. The account is how Coldtivate finds the customer's machines. Sign-ins that need an extra step, such as a one-time code, a captcha or a password change, aren't supported.
Each machine is enabled for Coldtivate's OAuth client by Ecozen. Otherwise the connection test fails with an access-rejection message naming the machine ID.
Each machine is online and reporting. The connection test needs at least one temperature reading from the last 24 hours.

Network

  • Outbound access: the Base API web and Celery worker containers need outbound HTTPS access to cloud.ecozen.ai, openid.ecozen.ai and apis.ecozen.ai. No inbound connection from Ecozen is needed.
  • Celery: the worker and beat must be running. Beat queues sensor polling every 4 hours, and the worker also runs the first poll queued when a machine is linked.

Settings

ECOZEN_CLIENT_ID and ECOZEN_CLIENT_SECRET are Base API settings. See Configuration. If either is empty, discovery still works, but connection tests fail with ECOZEN_CLIENT_CONFIGURATION and polling stops with a configuration error. Saving trusts a valid source ticket from a successful test and makes no OAuth request. The rest of the platform is unaffected.

Provider URLs and limits are constants in the code:

Constant Value Purpose
OAUTH_TOKEN_ENDPOINT (ecozen.py) https://openid.ecozen.ai/realms/ecozen/protocol/openid-connect/token Issues Coldtivate's access tokens.
OAUTH_GRAPH_ENDPOINT (ecozen.py) https://apis.ecozen.ai/dashboard/v3/ecofrost/graph/{machine_id} Reads a machine's readings.
CLOUD (ecozen.py) https://cloud.ecozen.ai/ Ecozen Cloud, used only for discovery.
TICKET_MAX_AGE (ecozen.py) 15 minutes Lifetime of each setup ticket.
SETUP_REQUEST_BUDGET_SECONDS (ecozen.py) 30 seconds Graph-request budget for a connection test, after runtime token resolution.
BUDGET_SECONDS (ecozen.py) 35 seconds Overall time limit for discovery.

3. Configuration

Step 1: set the OAuth client

Set ECOZEN_CLIENT_ID and ECOZEN_CLIENT_SECRET through the environment's normal secret mechanism, then restart the Base API web and Celery containers. Nothing is configured in the Django admin.

In the mobile app, as a Registered Employee of the company:

  1. Create or edit the cooling unit.
  2. Turn on Sensor available and choose ecozen.
  3. Enter the Ecozen Cloud username and password, then press Confirm. The fields are cleared once the request finishes.
  4. The app lists the account's machines, each with its label and ID. Use the search box to filter them, then select one.
  5. Press Test connection. The app checks the machine through Coldtivate's OAuth client, showing Testing connection… while it runs. Testing never saves anything.
    • If it fails, the reason is shown and the list stays open. For a machine Ecozen hasn't enabled, the message names the machine ID and asks the user to contact Coldtivate support.
    • If it succeeds, Connection confirmed is shown.
  6. Press Save, then save the cooling unit within 15 minutes of the test. If a device or source ticket is invalid or expired, the app automatically returns to sign-in; sign in and test the machine again.

Step 3: verify

  1. Polling: shortly after saving, in the Django admin under Storage → Sensor integrations, the integration should have a recent Last successful poll at and Polling error unchecked. Its Username and Source ID are both the machine ID, and its password is empty.
  2. Readings: open Cooling Units → Room Conditions for the cooling unit. Readings from the last 24 hours should appear once that first poll has run.
  3. Freshness: Last updated should keep advancing. If no new reading arrives for more than 12 hours, the screen shows a Stale warning.

Maintenance

  • Rotating the secret: change ECOZEN_CLIENT_SECRET and restart the containers. Valid tokens are reused from the shared cache. Cache keys include the client configuration, so the next poll obtains a token using the new secret rather than reusing one issued under the old secret. Polling history is kept.
  • Editing a cooling unit: changes that don't touch its sensor don't need the Ecozen sign-in again.
  • Changing the machine: choose Ecozen again, sign in and test the new machine. It starts a fresh polling history.
  • Moving a cooling unit:
    • Same company: allowed.
    • Different company: rejected until the Ecozen sensor is selected again or turned off.
  • Removing a machine: turn off the sensor on its cooling unit. A machine Ecozen stops enabling fails every poll and raises a sensor error notification.

4. Developer reference

Components

Component Location
Polling adapter (OAuth only, no Django models) base/apps/storage/services/sensors/ecozen.py (EcozenIntegration)
Access-token issuance base/apps/storage/services/sensors/ecozen.py (obtain_access_token)
Live machine check and its error codes base/apps/storage/services/sensors/ecozen.py (verify_machine_access)
Cloud discovery client and setup tickets base/apps/storage/services/sensors/ecozen.py (EcozenCloudClient, discover_ecozen_devices, ticket helpers)
Polling task base/apps/storage/tasks/sensor_polling.py (sync_sensor_data, poll_sensor_integration)
Source-list and test-connection endpoints base/apps/storage/views/sensors.py (SensorIntegrationViewSet, EcozenViewSet)
Cooling-unit validation and persistence base/apps/storage/serializers/cooling_unit.py, base/apps/storage/services/cooling_unit_setup.py
Mobile sign-in, machine list and test src/screens/Dashboard/Management/AddCoolingUnit/components/Sensors/ (components/SensorModal/components/EcozenForm.tsx, the shared components/SourceList.tsx, and sensorErrors.ts for messages and save-time recovery)

Adapter contract

EcozenIntegration follows the shared adapter contract. At poll time its runtime_credentials() hook reuses a valid cached access token or obtains one when needed, and builds the plain configuration:

{
    "type": "ecozen",
    "username": "EZM-0001",           # the machine ID
    "password": "<OAuth access token>",  # issued at runtime, never stored
    "source_id": "EZM-0001",          # the machine ID
}
  • authorize(): obtains an access token if the current one is missing or expired.
  • list_sources() / test_connection(): a live check of the configured machine. They return [{"id": "<machine_id>", "name": "<machine_id>"}] only if temperature readings from the last 24 hours come back.
  • validate_source(source_id, live=True): the same live check. A machine other than the configured one is rejected.
  • get_datums_from_source(source_id, since, until): reads Room_1_T (temperature), Room_1_H (humidity) and Set_T (set point) and returns normalized datums.

The connection test and polling build the adapter through the same resolve_runtime_credentials() path, using the same token and request code. Save verifies the source ticket and company permissions without constructing the adapter or contacting Ecozen.

OAuth client and tokens

  • Token request: a form-encoded POST to the token endpoint with client_id, client_secret and grant_type=client_credentials. The response's access_token is sent as Authorization: Bearer <token> on graph requests.
  • Reuse:
    • Tokens are shared through the Base API's Redis cache until their reported expires_in, minus a margin of min(60 seconds, half the lifetime).
    • A token with no expires_in is used only for the poll or check that obtained it, and is never shared.
    • Tokens are never stored on SensorIntegration rows.
  • Coordinated renewal:
    • One worker renews the token under a short cache lock. Other workers don't wait: a poll reschedules, and a connection test asks the user to retry.
    • If a graph request returns 401 with a reused token, that token is discarded and replaced at most once, then the request is retried once.
  • Rotation: cache keys are HMACs of the client configuration, so a rotated secret never reuses a token issued under the previous one.
  • Secret handling:
    • The secret is read from settings for each token request.
    • Request bodies, tokens and provider responses are never logged or reported. Only error types, status codes, machine IDs and standard OAuth error codes such as invalid_client are.
    • Discovery runs with HTTP debug logging and Sentry capture suppressed.

Graph requests and responses

Each request is a POST with JSON {"from": "YYYY-MM-DDTHH:MM:SSZ", "to": "...", "paramList": "..."}. A poll asks for all three parameters in one request ("Room_1_T,Room_1_H,Set_T"), while connection tests ask for Room_1_T only. Save makes no graph request. The response is a list of named series, each parsed from the same response:

[{"name": "Room_1_T", "data": [[1767225600000, 4.5], [1767226500000, 4.6]]}]
  • Time zone: the request window is padded by 14 hours, because how v3 interprets from/to hasn't been confirmed. Results are then filtered to the exact UTC window.
  • Rows:
    • Timestamps are Unix milliseconds, stored in UTC.
    • Rows with unusable numeric timestamps or missing, non-numeric or non-finite values are skipped before ingestion. Missing or non-numeric timestamps make the response malformed.
    • Malformed series or rows make the whole response malformed.
  • Set point: attached to the temperature reading with the same timestamp.
  • Envelopes: both the HTTP status and known JSON envelopes are checked, because Ecozen can return errors with HTTP 200.
Response Treated as Polling Connection test
{"message": "No data found for machine …"} No readings A successful poll with nothing stored ECOZEN_NO_RECENT_DATA
{"message": "No Access to the device(s) / API"}, HTTP 403/404, or a 401 after one token refresh Access rejected Non-retryable: the integration fails at once ECOZEN_ACCESS_REJECTED
Token endpoint 400/401/403, or missing client settings Client configuration Non-retryable ECOZEN_CLIENT_CONFIGURATION
429 from the token or graph endpoint Rate limited Starts the shared cooldown (see Rate limits), then a bounded retry once it ends ECOZEN_RATE_LIMITED, with retry_after_seconds
Timeout, network error, 5xx Provider unavailable Retried (see polling) ECOZEN_TIMEOUT, ECOZEN_SERVICE_UNAVAILABLE
Unexpected JSON shape Malformed Retried ECOZEN_MALFORMED_RESPONSE

Rate limits

Coordination defaults and optional environment overrides are owned by ecozen.py; only the client ID and secret are registered in Django settings. Coordination is shared through the Redis cache, so every web request and Celery worker sees the same state. Nothing sleeps: callers get the remaining wait instead.

  • Shared cooldown:
    • A 429 from the token or graph endpoint pauses the whole client before it sends anything else.
    • A valid Retry-After, in seconds or as an HTTP date, is kept exactly, however long.
    • Without one, consecutive 429s back off exponentially with jitter, from ECOZEN_RATE_LIMIT_BACKOFF_SECONDS (60 seconds) up to ECOZEN_RATE_LIMIT_MAX_BACKOFF_SECONDS (15 minutes). A successful request resets the backoff.
    • The shared deadline only ever moves later, in one atomic Redis update, so concurrent 429s can't shorten it. The cooldown is scoped to the client ID, so rotating the secret doesn't reset it.
    • Releasing a request slot and discarding a rejected token are atomic compare-and-delete operations, so a slow worker can't remove another worker's lease or a newly renewed token.
    • Right before each graph or token request, one atomic step checks the cooldown and takes the request slot or renewal lock. No request is admitted after a cooldown is stored; only requests already admitted can still be in flight. With the concurrency limit disabled (0), the cooldown is only read just before sending.
  • Connection tests: during a cooldown they return immediately with 503 ECOZEN_RATE_LIMITED, a retry_after_seconds field and a Retry-After header. The app shows the existing "Ecozen is limiting requests right now…" message.
  • Polling:
    • Stopped before sending: a poll stopped by the cooldown, a busy request slot or an in-progress token renewal is rescheduled for when the wait ends, plus up to ECOZEN_POLL_RESCHEDULE_JITTER_SECONDS (120 seconds) of jitter. It doesn't spend its failure-retry budget, keeps its watermark and raises no notification.
    • Long cooldowns: each reschedule is at most ECOZEN_POLL_MAX_RESCHEDULE_SECONDS (50 minutes), below the Celery broker's visibility timeout. Longer cooldowns become several shorter waits, each re-checking the deadline, so no request is sent early. After 8 deferrals a poll ends quietly, and the next scheduled dispatch queues it again.
    • A real 429: counts as one of the normal 3 bounded retries. Its countdown waits towards the cooldown instead of being capped at 30 minutes, and later steps cover any remainder. Repeated 429s still end in the usual polling error and sensor error notification.
  • Concurrency:
    • As a temporary, configurable policy, at most ECOZEN_MAX_CONCURRENT_REQUESTS (default 2) graph requests are in flight per client across all workers. This smooths the burst of polls queued every 4 hours.
    • It isn't a requests-per-second limit; Ecozen hasn't published one. Set it to 0 only if disabling the concurrency guard is appropriate for Ecozen's confirmed limits and the deployment's request volume.
  • Logs: they record only the classification, the 429 status, the endpoint and the cooldown length.

Cloud discovery

discover_ecozen_devices(username, password) returns the account's machines as {"id", "name"} pairs. For each call it:

  1. Signs in: opens a fresh session and follows Ecozen Cloud's sign-in to its openid.ecozen.ai login form, then submits the username and password. Only those hosts, over HTTPS, are allowed.
  2. Classifies the result:
    • A returned login form with an error message means wrong credentials.
    • A form that asks for a one-time code, a captcha or a new password, or any other unexpected sign-in step, is ECOZEN_LOGIN_UNSUPPORTED.
  3. Reads the machines: reads the account's machine list page by page. Each deviceName becomes the machine ID, and the location or description, when present, becomes the label.
  4. Stays bounded: stops at 35 seconds, 120 requests, 100 pages or 2 MB per response, and fails as unavailable if a page is inconsistent.
  5. Cleans up: clears the cookies and closes the session. Credentials, cookies and pages are never logged or stored.

This uses Ecozen Cloud's web interface, which Ecozen doesn't document as a third-party API. It's used only for discovery: polling never depends on it, and there's no portal-based fallback for polling.

API endpoints

Discover machines: POST /storage/v1/user-sensor/sources/

{
  "integration_type": "ecozen",
  "username": "<cloud username>",
  "password": "<cloud password>"
}
  • Access: any authenticated user, like the other providers' source listing. Whether the user may configure a given cooling unit is checked when it's saved.
  • Behaviour: Cloud sign-in and machine listing only. No v3 API calls.
  • Success (200):
    {
      "sources": [
        {"id": "EZM-0001", "name": "Cold room A — EZM-0001"},
        {"id": "EZM-0002", "name": "EZM-0002"}
      ],
      "device_token": "<signed device ticket>"
    }
    
  • Errors: always {"code": ..., "detail": ...}.
Status code When
401 ECOZEN_CLOUD_AUTHENTICATION_FAILED Username or password missing, or Ecozen Cloud rejected them.
404 ECOZEN_NO_CLOUD_DEVICES The account lists no machines.
409 ECOZEN_LOGIN_UNSUPPORTED The sign-in needs a step Coldtivate doesn't support.
503 ECOZEN_DISCOVERY_UNAVAILABLE Ecozen Cloud unreachable, rate limited, slow, or an unexpected page.

Test a machine: POST /storage/v1/ecozen/test-connection/

{"source_id": "EZM-0001", "device_token": "<signed device ticket>"}
  • Behaviour:
    1. It checks that the device ticket was issued to this user and lists this machine. No API call is made otherwise.
    2. It asks the v3 API for the machine's temperature over the last 24 hours. Only readings timestamped in that window count; older or future-dated ones don't.
  • Success (200):
    {"success": "Successfully connected", "source_token": "<signed source ticket>"}
    
  • Errors: always {"code": ..., "detail": ...}. Not-enabled machines use 409 rather than 422, because the mobile HTTP client logs request details on 422.
Status code When
400 ECOZEN_DISCOVERY_EXPIRED / ECOZEN_DISCOVERY_INVALID The ticket expired, was tampered with, belongs to another user, is a source ticket, or doesn't list this machine.
409 ECOZEN_ACCESS_REJECTED Ecozen hasn't enabled this machine for Coldtivate's client. The detail names the machine and asks the user to contact Coldtivate support.
409 ECOZEN_NO_RECENT_DATA No temperature reading in the last 24 hours, so access couldn't be confirmed.
503 ECOZEN_CLIENT_CONFIGURATION The OAuth client settings are missing, or Ecozen rejected the client.
502 ECOZEN_MALFORMED_RESPONSE An unexpected response shape.
503 / 504 ECOZEN_RATE_LIMITED, ECOZEN_SERVICE_UNAVAILABLE / ECOZEN_TIMEOUT A temporary provider failure.

Link a machine: create or update the cooling unit (POST /storage/v1/cooling-units/ or PUT /storage/v1/cooling-units/{id}/) with:

{
  "sensor": true,
  "sensor_data": {
    "type": "ecozen",
    "source_id": "EZM-0001",
    "username": "EZM-0001",
    "password": "",
    "source_token": "<signed source ticket>"
  }
}

The saved integration has username and source_id set to the machine ID, and an empty password. Tickets and Cloud credentials are never stored.

Read the sensor: GET /storage/v1/cooling-units/{id}/sensor-data/ returns source_id, type, username (the machine ID) and the link date. It never returns a password.

Setup tickets

Discovery uses two short-lived tickets, signed with Django signing. Each has its own salt, so one can never be used in place of the other.

  • Contents: only the user ID, provider type and machine IDs, plus the signing timestamp.
  • Not tied to a company: like with Inficold, a user can attach a machine they can see to any cooling unit they're allowed to configure.
Ticket Issued by Lists Accepted by
Device ticket (device_token, salt storage.ecozen.device-ticket.v1) Discovery Every machine in the account The connection test
Source ticket (source_token, salt storage.ecozen.source-ticket.v1) A successful connection test The tested machine Saving the cooling unit

Each ticket lasts 15 minutes from when it's issued, so the time to save starts at the connection test.

When a source ticket is required: adding Ecozen to a cooling unit, changing its machine, saving it under a different company, or any save that includes a source_token. Saving the unchanged machine in the same company without a ticket skips these checks.

Checks on save:

  1. The user has a staff role in the cooling unit's company. Otherwise sensor_data.source_token is "You cannot configure sensors for this company."
  2. The source ticket is valid, younger than 15 minutes, issued to this user and lists this machine.

There is no live recheck during save. The source ticket proves a successful temperature test within its 15-minute lifetime. Initial and scheduled polling independently check current API access.

Errors:

  • Ticket failures: return sensor_data.source_token and sensor_data.code, set to ECOZEN_DISCOVERY_EXPIRED or ECOZEN_DISCOVERY_INVALID. The app shows "Your Ecozen setup session expired…" and reopens the Ecozen sign-in.
  • Storage limit: a machine ID longer than the 64-character source_id column returns sensor_data.code: ECOZEN_MACHINE_ID_TOO_LONG and a readable sensor_data.source_id asking the user to contact support. Discovery and testing do not impose an assumed provider ID format; nonempty IDs are percent-encoded in graph URLs.

Polling and data

  • First poll: queued after the save commits. It covers the last 24 hours.
  • Later polls: run every 4 hours and request everything since the last stored reading, minus a 15-minute overlap. See Polling and ingestion.
  • Requests: each poll makes one graph request for all three parameters, plus a token request only when no shared token is valid. Requests have 5-second connect and 10- or 15-second read timeouts, within a 2-minute budget per poll.
  • Absent series: a missing humidity or set-point series supplies no readings and keeps the valid temperature readings. A malformed series fails the whole response.
  • Identity: changing the machine resets the polling watermark. Rotating the secret doesn't.

Migration from customer credentials

Earlier versions stored each customer's Ecozen username and password and used Ecozen's older login API. Migration storage.0073_clear_ecozen_credentials is a data-only cleanup:

  • What it does: for every Ecozen integration, it sets username to the machine ID (source_id) and clears password.
  • What it keeps: machine IDs, watermarks and polling state are unchanged, so polling resumes from the last stored reading.
  • What it doesn't touch: other providers' integrations.
  • Reversing it: a no-op. Cleared credentials can't be restored.

After the migration, every Ecozen integration polls through OAuth. Machines that Ecozen hasn't enabled for Coldtivate's client fail with an access rejection and raise a sensor error notification until Ecozen enables them.

Tests

  • Backend: offline, with mocked HTTP. Any real request fails the test.
    • services/sensors/test_sensors.py covers the shared adapter contract, OAuth, response parsing, runtime configuration and request coordination.
    • test_sensor_ingestion.py covers Cloud discovery, tickets, connection endpoints, save validation, polling, secret handling and the credential-cleanup migration.
  • Commands and live acceptance: see Testing.

5. Troubleshooting

Symptom Likely cause Fix
App: "Incorrect Ecozen Cloud username or password." Ecozen Cloud rejected the sign-in. Check the credentials by signing in to Ecozen Cloud directly.
App: "No machines were found in this Ecozen Cloud account." The account lists no machines. Ask Ecozen to assign the machines to that account.
App: "This Ecozen account requires an unsupported sign-in step…" The login asks for a one-time code, a captcha, a password change or another extra step. Complete that step in Ecozen Cloud, or use an account with a plain password sign-in.
App: "Ecozen machine discovery is unavailable…" Ecozen Cloud unreachable, slow, or its pages changed. Check outbound HTTPS to cloud.ecozen.ai and openid.ecozen.ai. If it persists, the Cloud interface may have changed. Already-linked machines keep polling.
Test connection: "Ecozen hasn't enabled API access to machine … for Coldtivate…" Coldtivate's OAuth client can't read that machine. Ask Ecozen to enable that machine ID for Coldtivate's client.
Test connection: "Ecozen returned no readings for machine … in the last 24 hours…" The machine hasn't reported recently. Confirm the machine is online in Ecozen Cloud, then test again.
App: "The Coldtivate Ecozen connection isn't available right now…" ECOZEN_CLIENT_ID/ECOZEN_CLIENT_SECRET missing or rejected. In the Base API logs, "Ecozen rejected the OAuth client credentials" means Ecozen rejected the client: verify both the client ID and secret, and confirm the client provisioning and grant permissions with Ecozen.
App: "Your Ecozen setup session expired…" More than 15 minutes passed since signing in (for the test) or since the test (for saving). Sign in to Ecozen again and test the machine again.
Saving the cooling unit fails with "You cannot configure sensors for this company." The user has no staff role in the target company. Use an account with a Registered Employee, site official or operator role in that company.
Stale warning, or a sensor error notification Polls are failing, or the machine stopped reporting. In the admin, check the integration's Polling error and Last successful poll at. Check the client settings, and confirm with Ecozen that the machine is enabled and reporting.

6. Rollout and known limitations

  • Coordinated release: deploy the Base API, including migration 0073, together with a mobile app version that has the Cloud sign-in.
    • Older app versions still send a username, password and typed machine ID to the connection test. They get 400 ECOZEN_DISCOVERY_INVALID and can't link new Ecozen machines until users update.
    • Existing linked machines keep polling whichever app version is installed.
  • Legacy Angular client: its Ecozen check calls /storage/v1/ecozen-test/, which has no backend route. It can't set up Ecozen sensors.
  • Undocumented Cloud interface: discovery depends on Ecozen Cloud's sign-in flow and web pages, which Ecozen doesn't document for third parties and may change. If they change, discovery fails as unavailable while polling continues.
  • Ownership: a machine is offered to anyone who can sign in to a Cloud account that lists it. Two companies could link the same machine.
  • Enablement is checked late: the machine list comes from Ecozen Cloud alone, so a machine that isn't enabled for Coldtivate only shows as such when it's tested.
  • No older history: only the last 24 hours are imported when a machine is linked.
  • Unconfirmed provider behaviour:
    • Humidity and set-point data from v3 haven't been confirmed with live data. A missing series counts as no readings, but an error on any of the three parameters fails the poll.
    • How v3 interprets the time zone of from/to hasn't been confirmed.
    • Ecozen's rate limits haven't been confirmed: whether they apply per client, machine or endpoint, and their Retry-After behaviour. Neither have its acceptance of the combined paramList, error envelopes beyond those listed above, or token lifetimes. Graph requests have returned 429 while token requests succeeded.
    • The concurrency and backoff defaults are cautious placeholders until Ecozen confirms its limits.
  • Fleet polling: the four-hour dispatcher queues every active integration together. The concurrency limit and shared cooldown do not enforce a requests-per-minute quota, provide fair scheduling, or guarantee that every machine updates each cycle. Shared request pacing and fair scheduling remain necessary for reliable fleet polling under a strict shared quota; confirm that quota and the supported polling frequency with Ecozen.
  • Machine-ID storage: Ecozen has not published an ID format, so discovery and testing accept nonempty strings. Saving remains limited by Coldtivate's 64-character source_id column; longer IDs require support and a schema change.
  • Live acceptance: automated tests use synthetic responses. A live connection test and background polling against an enabled machine are still needed before relying on the integration in production.