Skip to content

Configure the NATS Connection

This guide shows how to point a Py ToloMEO service at a broker, give it credentials, and secure the connection with TLS — all from a configuration file, with no code changes.

Where Configuration Comes From

Py ToloMEO reads a single TOML file, tolomeo.toml, once per process. The first source that exists wins:

  1. An explicit path passed to load_config()
  2. The TOLOMEO_CONFIG environment variable
  3. ./tolomeo.toml
  4. ~/.config/tolomeo/tolomeo.toml
  5. /etc/tolomeo/tolomeo.toml
  6. Built-in defaults

No file is a valid deployment

A missing configuration file is not an error — the built-in defaults apply and the service behaves exactly as it did before this feature existed.

A file that does exist but is malformed, names an unknown key, or gives a key the wrong type is an error, raised at startup and naming the file and the key. Configuration mistakes surface when the service starts, never as a surprise mid-run.

Change the Broker URL

Create tolomeo.toml next to your service:

[nats]
url = "nats://my-server:4222"

That is the whole change. No subclassing, no code edit, no rebuild.

Add Credentials

Choose exactly one authentication style.

[nats]
url = "nats://my-server:4222"
user = "alice"
password = "secret"
[nats]
url = "nats://my-server:4222"
token = "s3cr3t-token"
[nats]
url = "nats://my-server:4222"
credentials_file = "/etc/nats/app.creds"

One style at a time

Configuring two authentication styles at once is a configuration error. Silently preferring one over another would hide a deployment mistake.

Enable TLS

Point at a CA bundle, and optionally at the hostname the certificate is expected to present:

[nats]
url = "tls://my-server:4222"
tls_ca_file = "/etc/nats/ca.pem"
tls_hostname = "broker.example"

The framework builds the SSL context from that file at startup — the client library takes a prepared context, not a CA path. An unreadable or invalid CA file is a configuration error at startup, not a connection failure later.

Tune Reconnection

[nats]
reconnect_time_wait_s = 2   # seconds between attempts; this is the default
max_reconnect_attempts = 10 # omit to use the client library's own default (60)

Omitted means not passed

A key you leave out is omitted from the connect call entirely rather than passed as a value the framework guessed. That keeps the default deployment identical to the library's own behaviour, even across a library upgrade.

Set the Heartbeat Cadence

[service]
heartbeat_interval_ms = 10000  # the default

Pass Options to a Plugin

Each plugin reads its own table, keyed by its plugin id:

[plugins.thermostat]
endpoint = "http://localhost:9000"
samples = 4

The plugin declares which keys are valid as a Pydantic model, and reads the validated result through self.options:

class ThermostatOptions(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True, hide_input_in_errors=True)

    endpoint: str = "http://localhost:9000"
    samples: PositiveInt = 4


class ThermostatPlugin(SensorPlugin):
    class Meta:
        config_model = ThermostatOptions

A mistyped key stops the plugin as it attaches and names the offender, instead of sitting inert in a deployed file. See Implement a Custom Plugin and the Configuration Reference.

Shipped examples

The example services each ship a commented .toml beside them — temperature.toml, ota_mock.toml, ble_mock.toml — showing every table that service reads. Pass one with TOLOMEO_CONFIG.

Environment Variables

Variable Default Description
TOLOMEO_CONFIG unset Path to a configuration file
DEVICE_SERIAL_NUMBER 1122334455667788 Device identifier used in the SenML base name
TOLOMEO_CONFIG=/opt/myservice/tolomeo.toml \
DEVICE_SERIAL_NUMBER=abc123 \
python -m my_service.main

Pointing a Service at Its Own File

One process runs one service with one configuration. Name the file in the service's environment and it is read once at startup:

TOLOMEO_CONFIG=/opt/myapp/tolomeo.toml python -m my_service.main

Advanced: Subclassing

Configuration covers the broker address, credentials, TLS and reconnection. For anything it cannot express — a bespoke connect sequence, a custom transport, a value computed at runtime — subclass the client manager and wire it in through a strategy.

Prefer passing settings explicitly, which take precedence over the configuration file:

from tolomeo.messaging.client import NATSClientManager


class CustomNATSClientManager(NATSClientManager):
    def __init__(self, logger):
        super().__init__(logger, url=discover_broker_url())

Or override connect outright when the sequence itself must change:

import nats
from tolomeo.messaging.client import NATSClientManager


class CustomNATSClientManager(NATSClientManager):
    async def connect(self) -> None:
        self.client = await nats.connect(**self._connect_options())
        await self.do_something_bespoke()

Then pass your manager to a custom strategy and wire it into the service:

from tolomeo.messaging import NATSMessageStrategy
from tolomeo.services import NATSService


class CustomStrategy(NATSMessageStrategy):
    def __init__(self, logger=None):
        super().__init__(
            logger=logger,
            client_manager=CustomNATSClientManager(logger),
        )


class MyService(NATSService):
    class Meta:
        plugin_class = MyPlugin
        strategy_class = CustomStrategy

Publishing Cadence

Note

Py ToloMEO publishes sensor data as soon as a plugin pushes it via push_reading — the service reacts to data as it arrives. There is no polling interval to configure.

See also