Skip to content

Commit

Permalink
feat: environment precedence mechanism (#21)
Browse files Browse the repository at this point in the history
* chore: upgrade deps

* feat: env precedence

* chore: remove inspect

* chore: upgrade node
  • Loading branch information
webbertakken authored Aug 11, 2024
1 parent fb5a0df commit c740772
Show file tree
Hide file tree
Showing 16 changed files with 3,095 additions and 1,392 deletions.
1 change: 0 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
"unicorn/prefer-node-protocol": "off",
"unicorn/no-array-for-each": "off",
"unicorn/import-style": "off",
"sort-keys-fix/sort-keys-fix": "warn",
"unicorn/prefer-event-target": "off",
"simple-import-sort/imports": "warn",
"simple-import-sort/exports": "warn",
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@
/.vscode
/.env*
!/.env*.dist

# DigitalAlchemy
/synapse_storage.db
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
# If tty is available, apply fix from https://github.com/typicode/husky/issues/968#issuecomment-1176848345
if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then exec >/dev/tty 2>&1; fi

# Heavy checks should only be done on staged files123
# Heavy checks should only be done on staged files
bun run lint-staged
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ cd automation-standalone

### Install

**Optional**: If you don't have Volta installed, you must enable Corepack to use the correct Yarn
version.

```bash
npm unistall -g yarn pnpm
corepack enable
```

Install dependencies using Yarn:

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

```bash
yarn sync
yarn type-writer
```

### Run
Expand Down
29 changes: 17 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
"dev": "bun --hot --watch src/main.ts",
"play": "docker-compose -f playground/docker-compose.yml up",
"endplay": "docker-compose -f playground/docker-compose.yml down",
"sync": "yarn up \"@digital-alchemy/*\" && bunx --env-file .env type-writer",
"type-writer": "type-writer",
"build": "bun --env-file .env build:docker",
"build:dist": "bun build src/main.ts --compile --minify --outfile dist/server",
"build:docker": "docker build . --build-arg HASS_TOKEN=$HASS_TOKEN --build-arg HASS_BASE_URL=$HASS_BASE_URL -t automation-prod",
"upgrade": "yarn up \"@digital-alchemy/*\"",
"start": "docker run --env-file .env automation-prod",
"test": "vitest",
"coverage": "vitest --coverage",
Expand All @@ -35,22 +36,26 @@
"*.@(ts|tsx|mts|js|jsx|mjs|cjs|json|jsonc|json5|md|mdx|yaml|yml)": "prettier --write"
},
"dependencies": {
"@digital-alchemy/core": "^0.3.11",
"@digital-alchemy/hass": "^0.3.14",
"@digital-alchemy/synapse": "^0.3.5",
"dayjs": "^1.11.10"
"@digital-alchemy/automation": "^24.7.1",
"@digital-alchemy/core": "^24.7.2",
"@digital-alchemy/fastify-extension": "^24.7.1",
"@digital-alchemy/hass": "^24.8.1",
"@digital-alchemy/mqtt-extension": "^24.7.1",
"@digital-alchemy/synapse": "^24.8.1",
"@digital-alchemy/type-writer": "^24.7.2",
"dayjs": "^1.11.12"
},
"devDependencies": {
"@cspell/eslint-plugin": "^8.7.0",
"@digital-alchemy/type-writer": "^0.3.8",
"@types/async": "^3.2.24",
"@types/bun": "^1.1.0",
"@types/bun": "^1.1.6",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.7",
"@typescript-eslint/eslint-plugin": "7.6.0",
"@typescript-eslint/parser": "7.6.0",
"@types/node": "^22.2.0",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@vitest/coverage-v8": "^1.5.0",
"bun": "^1.1.22",
"bun": "^1.1.20",
"cross-env": "^7.0.3",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "^2.29.1",
Expand All @@ -73,7 +78,7 @@
"vitest": "^1.5.0"
},
"volta": {
"node": "20.16.0",
"node": "22.6.0",
"yarn": "4.4.0"
},
"packageManager": "[email protected]"
Expand Down
4 changes: 2 additions & 2 deletions playground/homeassistant/config/.storage/core.area_registry
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"aliases": [],
"floor_id": null,
"icon": null,
"id": "living_room",
"id": "livingRoom",
"labels": [],
"name": "Living Room",
"picture": null
Expand All @@ -33,4 +33,4 @@
}
]
}
}
}
40 changes: 40 additions & 0 deletions src/core/runtime-precedence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { TServiceParams } from '@digital-alchemy/core'

export function RuntimePrecedence({ logger, config, hass, lifecycle }: TServiceParams) {
// Whether this runtime is in development mode or not
const isDevelop = config.homeAutomation.NODE_ENV === 'development'

// When developing locally, the production runtime will pause and the development runtime will take over
// @ts-expect-error - Entity will be created by setting the state here.
const isDevelopmentActive = hass.refBy.id('binary_sensor.is_development_runtime_active')

// Block outgoing commands and most incoming messages in prod when dev overrides it.
isDevelopmentActive.onUpdate(() => {
if (isDevelopmentActive.state === 'on') {
logger.info('Development runtime takes over')
// dev takes over, prod pauses
hass.socket.pauseMessages = !isDevelop
} else {
logger.info('Resuming production runtime')
// prod resumes, dev pauses
hass.socket.pauseMessages = isDevelop
}
})

// Update the state on startup
lifecycle.onReady(() => {
if (isDevelop) isDevelopmentActive.state = 'on'
})

// Give the go ahead for production to take over again when shutting down
lifecycle.onPreShutdown(async () => {
if (!isDevelop) return

isDevelopmentActive.state = 'off'

const result = await isDevelopmentActive.nextState(5000)
if (!result) return logger.error(`Unable to verify that production runtime has taken over.`)

logger.info(`Production runtime has taken over. Development: ${result.state}`)
})
}
7 changes: 7 additions & 0 deletions src/core/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TServiceParams } from '@digital-alchemy/core'
import { Database } from 'bun:sqlite'

// This service will be loaded first. Use it to do any global setup.
export function Setup({ synapse }: TServiceParams) {
synapse.sqlite.setDriver(Database)
}
15 changes: 15 additions & 0 deletions src/core/utils/dayjs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable unicorn/prefer-export-from */
import dayjs from 'dayjs'
import advancedFormat from 'dayjs/plugin/advancedFormat'
import isBetween from 'dayjs/plugin/isBetween'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import weekOfYear from 'dayjs/plugin/weekOfYear'

dayjs.extend(weekOfYear)
dayjs.extend(advancedFormat)
dayjs.extend(isBetween)
dayjs.extend(utc)
dayjs.extend(timezone)

export { dayjs }
8 changes: 4 additions & 4 deletions src/entity-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ describe('EntityList', () => {
},
}
const logger = { debug: vi.fn(), info: vi.fn() }
const home_automation = {
helper: { doStuff: vi.fn(), theChosenEntity: { onUpdate: vi.fn() } },
const homeAutomation = {
helpers: { doStuff: vi.fn(), theSun: { onUpdate: vi.fn() } },
}

// @ts-expect-error these are not fully fledged out as this is a quick example
EntityList({ hass, home_automation, logger })
EntityList({ hass, homeAutomation, logger })
expect(hass.socket.onConnect).toHaveBeenCalledTimes(1)
expect(home_automation.helper.theChosenEntity.onUpdate).toHaveBeenCalledTimes(1)
expect(homeAutomation.helpers.theSun.onUpdate).toHaveBeenCalledTimes(1)
})
})
19 changes: 6 additions & 13 deletions src/entity-list.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
import { TServiceParams } from '@digital-alchemy/core'

/**
* There's other helpful things inside TServiceParams
*
* https://docs.digital-alchemy.app/TServiceParams
* https://docs.digital-alchemy.app/Hass
*/
export function EntityList({ hass, logger, home_automation }: TServiceParams) {
// note: helper must be loaded first
const { theChosenEntity } = home_automation.helper
export function EntityList({ hass, logger, homeAutomation }: TServiceParams) {
const { theSun } = homeAutomation.helpers

hass.socket.onConnect(async () => {
const resultText = home_automation.helper.doStuff()
const resultText = homeAutomation.helpers.doStuff()
const entities = hass.entity.listEntities()
logger.info({ entities, resultText }, 'hello world')
await hass.call.notify.notify({
message: 'Hello world from digital-alchemy',
})
})

theChosenEntity.onUpdate(() => {
theSun.onUpdate(() => {
logger.debug(
{
attributes: theChosenEntity.attributes,
state: theChosenEntity.state,
attributes: theSun.attributes,
state: theSun.state,
},
`theChosenEntity updated`,
)
Expand Down
30 changes: 0 additions & 30 deletions src/helper.ts

This file was deleted.

13 changes: 13 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TServiceParams } from '@digital-alchemy/core'

export function Helpers({ logger, config, hass }: TServiceParams) {
const theSun = hass.refBy.id('sun.sun')

const doStuff = (): string => {
logger.info('doStuff was called!')

return config.homeAutomation.MY_CONFIG_SETTING
}

return { theSun, doStuff }
}
81 changes: 30 additions & 51 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,58 @@
import { CreateApplication } from '@digital-alchemy/core'
import { LIB_AUTOMATION } from '@digital-alchemy/automation'
import { CreateApplication, StringConfig } from '@digital-alchemy/core'
import { LIB_HASS } from '@digital-alchemy/hass'
import { LIB_SYNAPSE } from '@digital-alchemy/synapse'

import { EntityList } from './entity-list'
import { HelperFile } from './helper'
import { RuntimePrecedence } from './core/runtime-precedence'
import { Setup } from './core/setup'
import { Helpers } from './helpers'
import { Office } from './office'

type AutomationEnvironments = 'development' | 'production' | 'test'

const HOME_AUTOMATION = CreateApplication({
/**
* keep your secrets out of the code!
* these variables will be loaded from your configuration file
*/
name: 'homeAutomation',
configuration: {
EXAMPLE_CONFIGURATION: {
NODE_ENV: {
type: 'string',
default: 'development',
enum: ['development', 'production', 'test'],
description: "Code runner addon can set with it's own NODE_ENV",
} satisfies StringConfig<AutomationEnvironments>,

MY_CONFIG_SETTING: {
default: 'foo',
description: 'A configuration defined as an example',
type: 'string',
},
},

/**
* Adding to this array will provide additional elements in TServiceParams
* for your code to use
*/
libraries: [
/**
* LIB_HASS provides basic interactions for Home Assistant
*
* Will automatically start websocket as part of bootstrap
*/
LIB_HASS,
],

/**
* must match key used in LoadedModules
* affects:
* - import name in TServiceParams
* - and files used for configuration
* - log context
*/
name: 'home_automation',

/**
* Need a service to be loaded first? Add to this list
*/
priorityInit: ['helper'],
// Plugins for TSServiceParams
libraries: [LIB_HASS, LIB_SYNAPSE, LIB_AUTOMATION],

/**
* Add additional services here
* No guaranteed loading order unless added to priority list
*
* context: ServiceFunction
*/
// Service initialization order
priorityInit: ['setup', 'runtimePrecedence', 'helpers'],
services: {
entity_list: EntityList,
helper: HelperFile,
setup: Setup,
runtimePrecedence: RuntimePrecedence,
helpers: Helpers,
office: Office,
},
})

// Load the type definitions
// Do some magic to make all the types work
declare module '@digital-alchemy/core' {
export interface LoadedModules {
home_automation: typeof HOME_AUTOMATION
homeAutomation: typeof HOME_AUTOMATION
}
}

// Kick off the application!
// bootstrap application
setImmediate(
async () =>
await HOME_AUTOMATION.bootstrap({
/**
* override library defined defaults
* not a substitute for config files
*/
configuration: {
// default value: trace
boilerplate: { LOG_LEVEL: 'debug' },
boilerplate: { LOG_LEVEL: 'info' },
},
}),
)
Loading

0 comments on commit c740772

Please sign in to comment.