Skip to content

Commit c740772

Browse files
authored
feat: environment precedence mechanism (#21)
* chore: upgrade deps * feat: env precedence * chore: remove inspect * chore: upgrade node
1 parent fb5a0df commit c740772

16 files changed

+3095
-1392
lines changed

.eslintrc.json

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
"unicorn/prefer-node-protocol": "off",
5757
"unicorn/no-array-for-each": "off",
5858
"unicorn/import-style": "off",
59-
"sort-keys-fix/sort-keys-fix": "warn",
6059
"unicorn/prefer-event-target": "off",
6160
"simple-import-sort/imports": "warn",
6261
"simple-import-sort/exports": "warn",

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@
99
/.vscode
1010
/.env*
1111
!/.env*.dist
12+
13+
# DigitalAlchemy
14+
/synapse_storage.db

.husky/pre-commit

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
# If tty is available, apply fix from https://github.com/typicode/husky/issues/968#issuecomment-1176848345
55
if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then exec >/dev/tty 2>&1; fi
66

7-
# Heavy checks should only be done on staged files123
7+
# Heavy checks should only be done on staged files
88
bun run lint-staged

README.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ cd automation-standalone
3939

4040
### Install
4141

42+
**Optional**: If you don't have Volta installed, you must enable Corepack to use the correct Yarn
43+
version.
44+
45+
```bash
46+
npm unistall -g yarn pnpm
47+
corepack enable
48+
```
49+
4250
Install dependencies using Yarn:
4351

4452
```bash
@@ -62,7 +70,7 @@ Then, configure each variable in `.env` so that the application can connect to y
6270
Synchronize the latest DA packages and write types based on your HA instance
6371

6472
```bash
65-
yarn sync
73+
yarn type-writer
6674
```
6775

6876
### Run

package.json

+17-12
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
"dev": "bun --hot --watch src/main.ts",
1010
"play": "docker-compose -f playground/docker-compose.yml up",
1111
"endplay": "docker-compose -f playground/docker-compose.yml down",
12-
"sync": "yarn up \"@digital-alchemy/*\" && bunx --env-file .env type-writer",
12+
"type-writer": "type-writer",
1313
"build": "bun --env-file .env build:docker",
1414
"build:dist": "bun build src/main.ts --compile --minify --outfile dist/server",
1515
"build:docker": "docker build . --build-arg HASS_TOKEN=$HASS_TOKEN --build-arg HASS_BASE_URL=$HASS_BASE_URL -t automation-prod",
16+
"upgrade": "yarn up \"@digital-alchemy/*\"",
1617
"start": "docker run --env-file .env automation-prod",
1718
"test": "vitest",
1819
"coverage": "vitest --coverage",
@@ -35,22 +36,26 @@
3536
"*.@(ts|tsx|mts|js|jsx|mjs|cjs|json|jsonc|json5|md|mdx|yaml|yml)": "prettier --write"
3637
},
3738
"dependencies": {
38-
"@digital-alchemy/core": "^0.3.11",
39-
"@digital-alchemy/hass": "^0.3.14",
40-
"@digital-alchemy/synapse": "^0.3.5",
41-
"dayjs": "^1.11.10"
39+
"@digital-alchemy/automation": "^24.7.1",
40+
"@digital-alchemy/core": "^24.7.2",
41+
"@digital-alchemy/fastify-extension": "^24.7.1",
42+
"@digital-alchemy/hass": "^24.8.1",
43+
"@digital-alchemy/mqtt-extension": "^24.7.1",
44+
"@digital-alchemy/synapse": "^24.8.1",
45+
"@digital-alchemy/type-writer": "^24.7.2",
46+
"dayjs": "^1.11.12"
4247
},
4348
"devDependencies": {
4449
"@cspell/eslint-plugin": "^8.7.0",
45-
"@digital-alchemy/type-writer": "^0.3.8",
4650
"@types/async": "^3.2.24",
47-
"@types/bun": "^1.1.0",
51+
"@types/bun": "^1.1.6",
4852
"@types/jest": "^29.5.12",
49-
"@types/node": "^20.12.7",
50-
"@typescript-eslint/eslint-plugin": "7.6.0",
51-
"@typescript-eslint/parser": "7.6.0",
53+
"@types/node": "^22.2.0",
54+
"@typescript-eslint/eslint-plugin": "7.18.0",
55+
"@typescript-eslint/parser": "7.18.0",
5256
"@vitest/coverage-v8": "^1.5.0",
53-
"bun": "^1.1.22",
57+
"bun": "^1.1.20",
58+
"cross-env": "^7.0.3",
5459
"eslint": "8.57.0",
5560
"eslint-config-prettier": "9.1.0",
5661
"eslint-plugin-import": "^2.29.1",
@@ -73,7 +78,7 @@
7378
"vitest": "^1.5.0"
7479
},
7580
"volta": {
76-
"node": "20.16.0",
81+
"node": "22.6.0",
7782
"yarn": "4.4.0"
7883
},
7984
"packageManager": "[email protected]"

playground/homeassistant/config/.storage/core.area_registry

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"aliases": [],
99
"floor_id": null,
1010
"icon": null,
11-
"id": "living_room",
11+
"id": "livingRoom",
1212
"labels": [],
1313
"name": "Living Room",
1414
"picture": null
@@ -33,4 +33,4 @@
3333
}
3434
]
3535
}
36-
}
36+
}

src/core/runtime-precedence.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { TServiceParams } from '@digital-alchemy/core'
2+
3+
export function RuntimePrecedence({ logger, config, hass, lifecycle }: TServiceParams) {
4+
// Whether this runtime is in development mode or not
5+
const isDevelop = config.homeAutomation.NODE_ENV === 'development'
6+
7+
// When developing locally, the production runtime will pause and the development runtime will take over
8+
// @ts-expect-error - Entity will be created by setting the state here.
9+
const isDevelopmentActive = hass.refBy.id('binary_sensor.is_development_runtime_active')
10+
11+
// Block outgoing commands and most incoming messages in prod when dev overrides it.
12+
isDevelopmentActive.onUpdate(() => {
13+
if (isDevelopmentActive.state === 'on') {
14+
logger.info('Development runtime takes over')
15+
// dev takes over, prod pauses
16+
hass.socket.pauseMessages = !isDevelop
17+
} else {
18+
logger.info('Resuming production runtime')
19+
// prod resumes, dev pauses
20+
hass.socket.pauseMessages = isDevelop
21+
}
22+
})
23+
24+
// Update the state on startup
25+
lifecycle.onReady(() => {
26+
if (isDevelop) isDevelopmentActive.state = 'on'
27+
})
28+
29+
// Give the go ahead for production to take over again when shutting down
30+
lifecycle.onPreShutdown(async () => {
31+
if (!isDevelop) return
32+
33+
isDevelopmentActive.state = 'off'
34+
35+
const result = await isDevelopmentActive.nextState(5000)
36+
if (!result) return logger.error(`Unable to verify that production runtime has taken over.`)
37+
38+
logger.info(`Production runtime has taken over. Development: ${result.state}`)
39+
})
40+
}

src/core/setup.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { TServiceParams } from '@digital-alchemy/core'
2+
import { Database } from 'bun:sqlite'
3+
4+
// This service will be loaded first. Use it to do any global setup.
5+
export function Setup({ synapse }: TServiceParams) {
6+
synapse.sqlite.setDriver(Database)
7+
}

src/core/utils/dayjs.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* eslint-disable unicorn/prefer-export-from */
2+
import dayjs from 'dayjs'
3+
import advancedFormat from 'dayjs/plugin/advancedFormat'
4+
import isBetween from 'dayjs/plugin/isBetween'
5+
import timezone from 'dayjs/plugin/timezone'
6+
import utc from 'dayjs/plugin/utc'
7+
import weekOfYear from 'dayjs/plugin/weekOfYear'
8+
9+
dayjs.extend(weekOfYear)
10+
dayjs.extend(advancedFormat)
11+
dayjs.extend(isBetween)
12+
dayjs.extend(utc)
13+
dayjs.extend(timezone)
14+
15+
export { dayjs }

src/entity-list.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ describe('EntityList', () => {
1111
},
1212
}
1313
const logger = { debug: vi.fn(), info: vi.fn() }
14-
const home_automation = {
15-
helper: { doStuff: vi.fn(), theChosenEntity: { onUpdate: vi.fn() } },
14+
const homeAutomation = {
15+
helpers: { doStuff: vi.fn(), theSun: { onUpdate: vi.fn() } },
1616
}
1717

1818
// @ts-expect-error these are not fully fledged out as this is a quick example
19-
EntityList({ hass, home_automation, logger })
19+
EntityList({ hass, homeAutomation, logger })
2020
expect(hass.socket.onConnect).toHaveBeenCalledTimes(1)
21-
expect(home_automation.helper.theChosenEntity.onUpdate).toHaveBeenCalledTimes(1)
21+
expect(homeAutomation.helpers.theSun.onUpdate).toHaveBeenCalledTimes(1)
2222
})
2323
})

src/entity-list.ts

+6-13
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,22 @@
11
import { TServiceParams } from '@digital-alchemy/core'
22

3-
/**
4-
* There's other helpful things inside TServiceParams
5-
*
6-
* https://docs.digital-alchemy.app/TServiceParams
7-
* https://docs.digital-alchemy.app/Hass
8-
*/
9-
export function EntityList({ hass, logger, home_automation }: TServiceParams) {
10-
// note: helper must be loaded first
11-
const { theChosenEntity } = home_automation.helper
3+
export function EntityList({ hass, logger, homeAutomation }: TServiceParams) {
4+
const { theSun } = homeAutomation.helpers
125

136
hass.socket.onConnect(async () => {
14-
const resultText = home_automation.helper.doStuff()
7+
const resultText = homeAutomation.helpers.doStuff()
158
const entities = hass.entity.listEntities()
169
logger.info({ entities, resultText }, 'hello world')
1710
await hass.call.notify.notify({
1811
message: 'Hello world from digital-alchemy',
1912
})
2013
})
2114

22-
theChosenEntity.onUpdate(() => {
15+
theSun.onUpdate(() => {
2316
logger.debug(
2417
{
25-
attributes: theChosenEntity.attributes,
26-
state: theChosenEntity.state,
18+
attributes: theSun.attributes,
19+
state: theSun.state,
2720
},
2821
`theChosenEntity updated`,
2922
)

src/helper.ts

-30
This file was deleted.

src/helpers.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { TServiceParams } from '@digital-alchemy/core'
2+
3+
export function Helpers({ logger, config, hass }: TServiceParams) {
4+
const theSun = hass.refBy.id('sun.sun')
5+
6+
const doStuff = (): string => {
7+
logger.info('doStuff was called!')
8+
9+
return config.homeAutomation.MY_CONFIG_SETTING
10+
}
11+
12+
return { theSun, doStuff }
13+
}

src/main.ts

+30-51
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,58 @@
1-
import { CreateApplication } from '@digital-alchemy/core'
1+
import { LIB_AUTOMATION } from '@digital-alchemy/automation'
2+
import { CreateApplication, StringConfig } from '@digital-alchemy/core'
23
import { LIB_HASS } from '@digital-alchemy/hass'
4+
import { LIB_SYNAPSE } from '@digital-alchemy/synapse'
35

4-
import { EntityList } from './entity-list'
5-
import { HelperFile } from './helper'
6+
import { RuntimePrecedence } from './core/runtime-precedence'
7+
import { Setup } from './core/setup'
8+
import { Helpers } from './helpers'
9+
import { Office } from './office'
10+
11+
type AutomationEnvironments = 'development' | 'production' | 'test'
612

713
const HOME_AUTOMATION = CreateApplication({
8-
/**
9-
* keep your secrets out of the code!
10-
* these variables will be loaded from your configuration file
11-
*/
14+
name: 'homeAutomation',
1215
configuration: {
13-
EXAMPLE_CONFIGURATION: {
16+
NODE_ENV: {
17+
type: 'string',
18+
default: 'development',
19+
enum: ['development', 'production', 'test'],
20+
description: "Code runner addon can set with it's own NODE_ENV",
21+
} satisfies StringConfig<AutomationEnvironments>,
22+
23+
MY_CONFIG_SETTING: {
1424
default: 'foo',
1525
description: 'A configuration defined as an example',
1626
type: 'string',
1727
},
1828
},
1929

20-
/**
21-
* Adding to this array will provide additional elements in TServiceParams
22-
* for your code to use
23-
*/
24-
libraries: [
25-
/**
26-
* LIB_HASS provides basic interactions for Home Assistant
27-
*
28-
* Will automatically start websocket as part of bootstrap
29-
*/
30-
LIB_HASS,
31-
],
32-
33-
/**
34-
* must match key used in LoadedModules
35-
* affects:
36-
* - import name in TServiceParams
37-
* - and files used for configuration
38-
* - log context
39-
*/
40-
name: 'home_automation',
41-
42-
/**
43-
* Need a service to be loaded first? Add to this list
44-
*/
45-
priorityInit: ['helper'],
30+
// Plugins for TSServiceParams
31+
libraries: [LIB_HASS, LIB_SYNAPSE, LIB_AUTOMATION],
4632

47-
/**
48-
* Add additional services here
49-
* No guaranteed loading order unless added to priority list
50-
*
51-
* context: ServiceFunction
52-
*/
33+
// Service initialization order
34+
priorityInit: ['setup', 'runtimePrecedence', 'helpers'],
5335
services: {
54-
entity_list: EntityList,
55-
helper: HelperFile,
36+
setup: Setup,
37+
runtimePrecedence: RuntimePrecedence,
38+
helpers: Helpers,
39+
office: Office,
5640
},
5741
})
5842

59-
// Load the type definitions
43+
// Do some magic to make all the types work
6044
declare module '@digital-alchemy/core' {
6145
export interface LoadedModules {
62-
home_automation: typeof HOME_AUTOMATION
46+
homeAutomation: typeof HOME_AUTOMATION
6347
}
6448
}
6549

66-
// Kick off the application!
50+
// bootstrap application
6751
setImmediate(
6852
async () =>
6953
await HOME_AUTOMATION.bootstrap({
70-
/**
71-
* override library defined defaults
72-
* not a substitute for config files
73-
*/
7454
configuration: {
75-
// default value: trace
76-
boilerplate: { LOG_LEVEL: 'debug' },
55+
boilerplate: { LOG_LEVEL: 'info' },
7756
},
7857
}),
7958
)

0 commit comments

Comments
 (0)