Metadata-Version: 2.4
Name: pysaes-neg-power-tspspi
Version: 0.0.1
Summary: Standalone MQTT monitoring and explicit activation control for SAES NEG POWER MINI (unofficial)
Author-email: Thomas Spielauer <pypipackages01@tspi.at>
License: BSD-3-Clause
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: pyserial>=3.5
Requires-Dist: paho-mqtt>=2.0
Dynamic: license-file

# SAES Neg Power MQTT Bridge (Unofficial)

Standalone MQTT monitoring and explicit activation control for **SAES NEG POWER
MINI**. No `PowerSupply` or pressure-gauge inheritance. A private internal
adapter translates MQTT commands into Modbus TCP or RS232/RTU transactions.
Connecting, disconnecting, restarting the service and recovering a connection
never automatically start or stop heating.

The manual documents the interfaces but omits the register map. The implementation
uses a map recovered from the supplied manager software; see [docs/protocol.md](docs/protocol.md)
for evidence, defaults, units and unresolved details. Tests use local dummy devices.
This has not been deployed or qualified against a physical NEG controller.

Install from this directory with `python3 -m pip install .` or `python3 -m pip install pysaes-neg-power-tspspi`. Copy
[the example configuration](examples/negpowermqtt.example.json), give the service
user ownership, and use mode 0600. Create the configured `state_directory` owned
by that same user with mode 0700. Run:

```sh
negpowermqtt --config /path/to/negpowermqtt.conf
```

Each device entry gets an independent polling/reconnect worker. Add additional
entries with unique names and endpoints for additional controllers. For serial,
replace `device_host` with `serial_port`; optional `baudrate`, `parity`, and
`stopbits` default to 38400, N, 1. Ethernet defaults to port 502 and unit 1;
the configured device values must agree with the front panel.

Control defaults to disabled. Set `control_enabled: true` for a device when
using external publishers to control it. Queries and monitoring work while
control is disabled. No hardware writes have been performed during development.

## MQTT topics

For base `/lab` and name `neg1`:

| Topic | Direction | Payload |
| --- | --- | --- |
| `/lab/neg/neg1/state` | Service publishes | Timestamp, online flag, identity, status, settings |
| `/lab/neg/neg1/availability` | Service publishes | Online flag and timestamp; MQTT last will also reports disconnect |
| `/lab/neg/neg1/command` | External publisher sends | JSON command envelope below |
| `/lab/neg/neg1/result` | Service publishes | Request id, result, timestamp, details/readback |

Publications use QoS 1 and are not retained. Periodic state is refreshed every
10 seconds by default. Query requests trigger a fresh snapshot. An offline
device sends availability=false and retries; no cached state is substituted.
Check timestamps; broker outages can delay or duplicate MQTT deliveries.

Commands require a unique id, an action, and Unix `issued_at`/`expires_at` values
with validity at most 60 seconds. Publish with **retain=false**. Retained
subscription deliveries, expired/future commands, malformed settings and unknown
actions/fields are rejected. No topic permits arbitrary register access.

Actions:

- `query`: obtain a fresh state/settings snapshot.
- `set_settings`: set and verify voltage, ramp, hold and maximum temperature;
  require standby; leave output state unchanged.
- `start`: set and verify the complete supplied settings, check standby,
  alarms, thermocouple and current temperature, then start the controller's ramp.
- `stop`: explicitly request output stop and return status readback.

`start` executes the controller's voltage ramp and hold. The maximum temperature
is a controller cutoff, not a PID temperature target. A valid thermocouple and
firmware support are required. The service does not create pump recipes, infer
settings from pump type, clear alarms, or resume a ramp after reboot. Use the
activation parameters specified for the actual pump. Hold zero explicitly selects
indefinite operation; stopping the daemon does not terminate it.

Example start payload (timestamps must be generated when publishing):

```json
{
  "id": "activation-unique-id",
  "action": "start",
  "issued_at": 1700000000,
  "expires_at": 1700000030,
  "settings": {
    "voltage_V": 8.8,
    "ramp_minutes": 15,
    "hold_minutes": 60,
    "max_temperature_K": 573
  }
}
```

These settings demonstrate the schema; they are not a recommended pump recipe.
Temperature is an integer kelvin value on the wire (573 K is approximately
300 degrees Celsius). State also reports the measured temperature in Celsius.

`completed` means a query or settings transaction completed with readback.
`accepted` means start/stop was acknowledged and status was read back; it does
not mean activation finished or the temperature cutoff was physically verified.
`rejected` means the service did not accept the command. `uncertain` means writes
may have executed or settings may be partial; query state before deciding what
to do next. An uncertain start is never resent automatically.

The SQLite journal records each accepted request id before its first possible
write. Exact duplicates return the saved result without replay; reusing an id
with different content is rejected. This survives service restarts and lost
result delivery. An interrupted claimed request remains uncertain. Do not delete
the journal to retry a command. Offline commands are rejected, not saved for
execution when the device returns. Commands queued across a broker reconnect are
discarded. A retry uses the same id, timestamps and payload; an expired retry is
rejected. Only issue a new control id after reconciling the device state.

The broker must authenticate publishers and enforce topic permissions: service
accounts subscribe only to their command topics and publish only state,
availability and result; observer accounts read those publications; control
accounts publish only intended command topics. The service cannot identify a
publisher from a normal MQTT message, so access control belongs at the broker.
Use one command owner per controller; front-panel or other software changes can
race separate Modbus transactions. MQTT delivery is not a real-time interlock;
use the controller's hardware protections.

See [examples/publish_command.py](examples/publish_command.py) for an external
publisher using its own private credentials file. A FreeBSD rc.d template is
provided at [service/negpowermqtt](service/negpowermqtt); no services or users have
been installed by creating this repository.

## Development

```sh
PYTHONPATH=src python3 -m pytest -q
python3 -m build
```

Tests cover TCP/RTU framing, settings validation/readback, temperature/start
checks, retained/expired commands, persistent deduplication, uncertain writes,
multiple devices, and real Paho MQTT interaction with a local dummy broker.
