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.aiandapis.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.
Step 2: link a machine to a cooling unit
In the mobile app, as a Registered Employee of the company:
- Create or edit the cooling unit.
- Turn on Sensor available and choose ecozen.
- Enter the Ecozen Cloud username and password, then press Confirm. The fields are cleared once the request finishes.
- The app lists the account's machines, each with its label and ID. Use the search box to filter them, then select one.
- 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.
- 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
- 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.
- Readings: open Cooling Units → Room Conditions for the cooling unit. Readings from the last 24 hours should appear once that first poll has run.
- 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_SECRETand 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): readsRoom_1_T(temperature),Room_1_H(humidity) andSet_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
POSTto the token endpoint withclient_id,client_secretandgrant_type=client_credentials. The response'saccess_tokenis sent asAuthorization: Bearer <token>on graph requests. - Reuse:
- Tokens are shared through the Base API's Redis cache until their reported
expires_in, minus a margin ofmin(60 seconds, half the lifetime). - A token with no
expires_inis used only for the poll or check that obtained it, and is never shared. - Tokens are never stored on
SensorIntegrationrows.
- Tokens are shared through the Base API's Redis cache until their reported
- 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
401with 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_clientare. - 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:
- Time zone: the request window is padded by 14 hours, because how v3 interprets
from/tohasn'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
429from 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 toECOZEN_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.
- A
- Connection tests: during a cooldown they return immediately with
503 ECOZEN_RATE_LIMITED, aretry_after_secondsfield and aRetry-Afterheader. 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.
- 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
- 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
0only if disabling the concurrency guard is appropriate for Ecozen's confirmed limits and the deployment's request volume.
- As a temporary, configurable policy, at most
- Logs: they record only the classification, the
429status, 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:
- Signs in: opens a fresh session and follows Ecozen Cloud's sign-in to its
openid.ecozen.ailogin form, then submits the username and password. Only those hosts, over HTTPS, are allowed. - 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.
- Reads the machines: reads the account's machine list page by page. Each
deviceNamebecomes the machine ID, and the location or description, when present, becomes the label. - Stays bounded: stops at 35 seconds, 120 requests, 100 pages or 2 MB per response, and fails as unavailable if a page is inconsistent.
- 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/
- 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): - 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/
- Behaviour:
- It checks that the device ticket was issued to this user and lists this machine. No API call is made otherwise.
- 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): - Errors: always
{"code": ..., "detail": ...}. Not-enabled machines use409rather than422, because the mobile HTTP client logs request details on422.
| 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:
- The user has a staff role in the cooling unit's company. Otherwise
sensor_data.source_tokenis "You cannot configure sensors for this company." - 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_tokenandsensor_data.code, set toECOZEN_DISCOVERY_EXPIREDorECOZEN_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_idcolumn returnssensor_data.code: ECOZEN_MACHINE_ID_TOO_LONGand a readablesensor_data.source_idasking 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
usernameto the machine ID (source_id) and clearspassword. - 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.pycovers the shared adapter contract, OAuth, response parsing, runtime configuration and request coordination.test_sensor_ingestion.pycovers 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_INVALIDand can't link new Ecozen machines until users update. - Existing linked machines keep polling whichever app version is installed.
- Older app versions still send a username, password and typed machine ID to the connection test. They get
- 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/tohasn't been confirmed. - Ecozen's rate limits haven't been confirmed: whether they apply per client, machine or endpoint, and their
Retry-Afterbehaviour. Neither have its acceptance of the combinedparamList, error envelopes beyond those listed above, or token lifetimes. Graph requests have returned429while 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_idcolumn; 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.