Light automation
A real-world automation: ensure minimum brightness in an office. This example demonstrates secrets, reusable resource recipes, external service integration, and cron scheduling.
Imports
load("pkg.flowmetal.io/prelude/v1",
"args",
"flow",
"FlowContext",
"resource",
"unwrap",
)
load("pkg.flowmetal.io/cron/v1", "cron")
load("pkg.flowmetal.io/home-assistant/v1", "home_assistant_resource")
load("pkg.flowmetal.io/one-password/v1", "op_resource")
Reusable resource recipe
resource() is to inline resource lambdas as flow() is to normal defs — a declarative, composable recipe. This wraps a Home Assistant client to pull its API key from 1Password at open time.
hass = resource(
open = lambda ctx: home_assistant_resource(
url = ctx.args.url,
token = ctx.resources.op.item(
ctx.args.vault_id,
ctx.args.secret_id,
).field("api-key").value,
),
close = lambda self: self.close(),
args = {
"url": args.string(),
"vault_id": args.string(),
"secret_id": args.string(),
"op": args.resource(op_resource),
},
)
Flow declaration
The flow declares its arguments, secrets, and resources up front. Secrets are validated at startup — if any are missing, the flow fails immediately rather than halfway through execution. Resource recipes can depend on other resources and on secrets; the runtime manages the dependency order.
auto_lights = flow(
args = {
"hass_url": args.string(),
"hass_secret_id": args.string(),
"luminosity_sensor_id": args.string(),
"luminosity_threshold": args.int(min = 0, default = 100),
"occupancy_sensor_id": args.string(),
"light_id": args.string(),
"light_on_data": args.json(
schema = {"properties": {"brightness_pct": {"type": "int"}}},
default = {"brightness_pct": 100},
),
},
secrets = ["op_token", "op_url", "op_vault"],
resources = {
"op": lambda fctx: op_resource(
url = fctx.secrets.op_url,
token = fctx.secrets.op_token,
),
"hass": lambda fctx: hass(
url = fctx.args.hass_url,
vault_id = fctx.secrets.op_vault,
secret_id = fctx.args.hass_secret_id,
op = fctx.resources.op,
),
},
implementation = _auto_lights_impl,
)
Implementation
The implementation is short — read sensors, make a decision, act. All the complexity (authentication, resource lifecycle, failure recovery) is handled by the declarations above.
def _auto_lights_impl(fctx: FlowContext):
occ_state = fctx.resources.hass.states(fctx.args.occupancy_sensor_id)
lux_state = fctx.resources.hass.states(fctx.args.luminosity_sensor_id)
if unwrap(occ_state) and unwrap(lux_state) < fctx.args.luminosity_threshold:
fctx.resources.hass.service(
"light/turn_on",
fctx.args.light_id,
data = fctx.args.light_on_data,
)
One-shot and scheduled execution
Call the flow directly for a one-shot run. For recurring execution, cron() is just a library flow — it computes the next fire time, sleep()s, calls your function, and repeats. Checkpointing makes it durable. spawn() detaches from the supervision tree so the cron loop outlives this script.
def _main_impl(fctx: FlowContext):
# One-shot
auto_lights(
hass_url = "https://hass.tirefireind.us",
hass_secret_id = "<redacted>",
occupancy_sensor_id = "<redacted>",
luminosity_sensor_id = "<redacted>",
luminosity_threshold = 100,
light_id = "<redacted>",
light_on_data = {"brightness_pct": 35},
)
# Scheduled: every 5 minutes
cron_ref = unwrap(fctx.actions.spawn(
lambda: cron(
pattern = "*/5 * * * *",
on_tick = lambda: auto_lights(
hass_url = "https://hass.tirefireind.us",
hass_secret_id = "<redacted>",
occupancy_sensor_id = "<redacted>",
luminosity_sensor_id = "<redacted>",
luminosity_threshold = 100,
light_id = "<redacted>",
light_on_data = {"brightness_pct": 35},
),
),
task_id = "lights-cron",
on_conflict = "replace",
))
print(cron_ref)
main = flow(implementation = _main_impl)
if __name__ == "__flow__":
main()