Skip to main content

Project Updates - 2024-04

ยท 3 min read
Zoe Codez
Author of Digital Alchemy

๐Ÿš€ Recent Improvementsโ€‹

๐Ÿ“ˆ Editor performanceโ€‹

The type-writer script now provides more information about your setup in the form of pre-built string unions. Your editor needs to do much less inferring work during development, resulting in dramatically better performance around in setups with larger quantities of entities

๐Ÿท Label Supportโ€‹

The project now has direct support for Home Assistant's new label feature, as well as bringing matching support for existing area, floor, and device. You are able to manage label & area for entities through convenient APIs, making for some easy migrations & writing quick batch operations for your system. Matching querying methods have also been provided:

  • hass.entity.byArea
  • hass.entity.byFloor
  • hass.entity.byDevice
  • hass.entity.byLabel

All of these methods are set up so they return string unions representing what you will receive at runtime. You are also able to filter by domain, quickly extracting a subset of entities from the existing groups.

  • Simple lookup by label: img

  • Looking up switches by area

lookup errorfilter by domain
โ›” Editor will let you know when you are using invalid entitiesโœ… quickly filter by domain

๐Ÿ‘ฅ Community updatesโ€‹

A few new sections in discord have been added recently better support newcomers to the project.

  • help - general q&a forum, get quick answers about the system
  • code review - want a second set of eyes to make sure your new automation is going to do what you think it should?

๐Ÿšง Current developmentโ€‹

๐ŸชŸ Automation Standaloneโ€‹

The current Automation Quickstart is intended for HAOS based setups, providing a quick setup script to bring your setup to a running state with a single command.

This new / second quickstart project is aimed at people invested in docker based setups. Target audiences:

  • docker based Home Assistant setups
  • people who want a template tuned to building / deploying your logic to a container
  • anyone looking to get the most out of their setup

Has support for the high performance Bun runtime, dedicated testing workflows, and built with Windows friendly workflows.

๐Ÿค– Unit testing workflowsโ€‹

The unit testing tools surrounding the hass library are receiving active attention, with the intent of creating set of tools that can be used to unit test your automations directly. This will be taken advantage of as part of the new quickstart project so you can include test coverage on the list of things you can flex about your setup ๐Ÿ’ช

Building a basic Automation

ยท 4 min read
Zoe Codez
Author of Digital Alchemy

This guide is part of a series. Check out the previous steps here

  1. Check out the quickstart guide to create your own project
  2. The quickstart](/automation-quickstart/next-steps) page can give you more context on how these work

Now that we have a foundation on what is a service / how to wire them together, let's build on that by creating a basic automation.

๐ŸŒ Connecting to Home Assistantโ€‹

For this, we'll need to import the hass library. If you used the quickstart project, this should already be set up for you.

import { CreateApplication } from "@digital-alchemy/core";
// Import library definition
import { LIB_HASS } from "@digital-alchemy/hass";

const SUPER_AWESOME_APP = CreateApplication({
// add to libraries
libraries: [LIB_HASS],
name: "my_super_awesome_app",
// ...
});

๐ŸŽ‰ What changed:

  • your application will connect to home assistant during bootstrap
  • hass is added to TServiceParams, providing you basic tools to interact with home assistant with

๐Ÿค– Creating logicโ€‹

๐ŸŒ‹ Responding to eventsโ€‹

It's finally time to the application do something productive! Let's start out with taking a look a super basic automation to get a feel for the grammar.

[!example] #Usage-Example/hass

import { TServiceParams } from "@digital-alchemy/core";

export function BasicAutomation({ hass, logger }: TServiceParams) {
const mySensor = hass.entity.byId("binary_sensor.my_example_sensor");

mySensor.onUpdate(async (new_state, old_state) => {
logger.info(
`my_example_sensor updated ${old_state.state} => ${new_state.state}`,
);
await hass.call.switch.toggle({
entity_id: "switch.example_switch",
});
});
}

In this example, an entity reference was created, with an update listener attached to it. The provided new_state & old_state variables reflect the states for that particular update, and while mySensor can be also used to directly access current state.

Now for more complex example, setting up a temporary schedule while a condition is true.

import { TServiceParams } from "@digital-alchemy/core";

// 5 MINUTES
const REPEAT_NOTIFICATION_INTERVAL = 1000 * 60 * 5;

export function GaragePester({ scheduler, logger, hass, internal }: TServiceParams) {
const isHome = hass.entity.byId("binary_sensor.i_am_home");
const garageIsOpen = hass.entity.byId("binary_sensor.garage_is_open");
let stop: () => void;

// UPDATE TRIGGER
isHome.onUpdate((new_state, old_state) => {
if (new_state.state === "off") {
// am home, stop notifying and clean up
if (stop) {
logger.info("welcome back home!");
stop();
stop = undefined;
}
return;
}
if (old_state.state !== "off" || stop) {
return;
}

// send a notification every 5 minutes
// ex: "You left 20m ago with the garage open"
const notifyingSince = new Date();
stop = scheduler.interval({
async exec() {
logger.info("still a problem");
// calculate a friendly string that describes how long
const timeAgo = internal.utils.relativeDate(notifyingSince);

// call the `notify.notify` service
await hass.call.notify.notify({
message: `You left ${timeAgo} with the garage open`,
title: "Come back and close the garage!",
});
},
interval: REPEAT_NOTIFICATION_INTERVAL,
});
});

garageIsOpen.onUpdate(() => {
// stop notifying if I remotely close the garage
if (garageIsOpen.state === "off" && stop) {
logger.info("stopping garage reminders");
stop();
stop = undefined;
}
});
}

In this example, the service will track a pair of binary_sensor entities. If the combination indicates that I am both away, and the garage door is left open, then it will set up a temporary schedule.

If the situation changes, then the timer is stopped ๐ŸŽ‰

โฐ Timersโ€‹

Timers don't need to just be set in response to events, they can be a central feature of the way your application works. Send morning reports, make events that happen at "2ish"

[!example] #Usage-Example/core

import { CronExpression, sleep, TServiceParams } from "@digital-alchemy/core";

export function WeatherReport({ scheduler, logger, hass }: TServiceParams) {
const forecast = hass.entity.byId("weather.forecast_home");

async function SendWeatherReport() {
const [today] = forecast.attributes.forecast;
const unit = forecast.attributes.temperature_unit;
const message = [
`Today's weather will be ${today.condition}`,
`High: ${today.temperature}${unit} | Low: ${today.templow}${unit}`,
`Precipitation: ${today.precipitation * 100}%`,
// Hopefully with a perfect afternoon
].join("\n");
logger.info({ message }, "sending weather report");
await hass.call.notify.notify({
message,
title: `Today's weather report`,
});
}

scheduler.cron({
async exec() {
await SendWeatherReport();
},
schedule: CronExpression.EVERY_DAY_AT_8AM,
});

scheduler.cron({
async exec() {
// Generate random number 0-30
const waitMins = Math.floor(Math.random() * 30);
logger.debug(`sleeping ${waitMins} minutes`);
await sleep(waitMins * 1000 * 60); // ๐Ÿ˜ด

logger.info("doing the thing!");
// maybe turn off if you need some rain? ๐ŸŒง
await hass.call.switch.turn_on({
entity_id: "switch.perfect_weather_machine",
});
},
schedule: CronExpression.EVERY_DAY_AT_2PM,
});
}

See Scheduler for more specific documentation.

๐ŸŽฌ Bringing it all togetherโ€‹

Time to bring it all together back in your application definition. If it isn't added there, it won't run!

import { CreateApplication } from "@digital-alchemy/core";
import { LIB_HASS } from "@digital-alchemy/hass";

const SUPER_AWESOME_APP = CreateApplication({
libraries: [LIB_HASS],
name: "my_super_awesome_app",
services: {
BasicAutomation,
GaragePester,
WeatherReport,
}
});

declare module "@digital-alchemy/core" {
export interface LoadedModules {
my_super_awesome_app: typeof SUPER_AWESOME_APP;
}
}

setImmediate(
async () =>
await SUPER_AWESOME_APP.bootstrap(),
);

That's it! Run your code and enjoy your new super awesome app ๐Ÿ˜‰


  • #Blog