-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(summary): Basic first device summary functionality
- Loading branch information
1 parent
690471f
commit 4afe97a
Showing
9 changed files
with
341 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/** | ||
* Returns an array of entities that match a set of filters | ||
* @param {Array} states - An array of state objects. | ||
* @param {Array} filters - An array of filter objects, each containing a key and a value. | ||
* @param {string} returnProperty - (Optional) A property to return from each entity. | ||
* @param {boolean} returnState - (Optional) If true, the state of each entity will be returned in addition to the returnProperty. | ||
* @returns {Array} - An array of entities that match the filter criteria. | ||
*/ | ||
export function getEntities(states, filters = [], returnProperty = null, returnState= false) { | ||
if (!Array.isArray(filters)) { | ||
throw new Error('Filters must be an array'); | ||
} | ||
if(filters.filter(filter => !filter.key || !filter.value).length > 0) { | ||
throw new Error('Filters must contain a key and a value'); | ||
} | ||
let entities = addDomainToEntities(states); | ||
filters.forEach(filter => { | ||
entities = entities.filter(entity => { | ||
const keys = filter.key.split('.'); | ||
let value = entity; | ||
for (const key of keys) { | ||
value = value[key]; | ||
} | ||
return value === filter.value; | ||
}); | ||
}); | ||
if (returnProperty) { | ||
entities = entities.map(entity => { | ||
const keys = returnProperty.split('.'); | ||
let value = entity; | ||
for (const key of keys) { | ||
value = value[key]; | ||
} | ||
return (returnState) ? `${value} (${entity.state})` : value; | ||
}); | ||
} | ||
return entities; | ||
} | ||
/** | ||
* Adds a domain property to each entity in an array of state objects. | ||
* @param {Array} states - An array of state objects. | ||
* @returns {Array} - An array of state objects with a domain property added to each entity. | ||
*/ | ||
export function addDomainToEntities(states) { | ||
return states.map(el => { | ||
el.domain = el.entity_id.split('.')[0]; | ||
return el; | ||
}); | ||
} | ||
|
||
/** | ||
* Returns a summary of the state of each entity in an array of state objects. | ||
* @param {Array} states - An array of state objects. | ||
* @returns {Array} - An array of strings summarizing the state of each entity. | ||
*/ | ||
export function summarizeEntities(states) { | ||
return states.map(el => { | ||
const attributes = Object.keys(el.attributes).map(key => { return `${key}: ${el.attributes[key]}`}); | ||
return `${el.entity_id}: ${el.state} (${attributes.join(', ')})`; | ||
}); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,19 @@ | ||
|
||
// TODO: Switch to winston, use ENV_VAR to set log level we want to see | ||
|
||
/** | ||
* Log a message to the console with proper formatting | ||
*/ | ||
|
||
export async function log(category, severity, ...messages) { | ||
|
||
//if(severity === 'debug') { return null; } | ||
const emojiForCategory = { | ||
'openai': '🧠', | ||
'ha': '🏠', | ||
'app': '🤖', | ||
'prompt': '🗣️', | ||
'response': '📢', | ||
} | ||
return console.log(emojiForCategory[category], `[${category.toUpperCase()}]`, ...messages); | ||
const emoji = emojiForCategory[category] || ''; | ||
return console.log(emoji, `[${category.toUpperCase()}]`, ...messages); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { | ||
LANGUAGE, | ||
OPENAI_MODEL, | ||
} from '../config'; | ||
import {log} from './logs'; | ||
|
||
/** | ||
* Get the first chat response from OpenAI API | ||
* @param {Object} openai - OpenAI API instance | ||
* @param {string} prompt - Prompt to send to the API | ||
* @param {Object} paramOptions - Options for the prompt | ||
* @returns {Promise<Object>} - Promise that resolves to the chat completion object | ||
*/ | ||
|
||
export async function getFirstChatResponse(openai, prompt, paramOptions ) { | ||
if(!openai) { | ||
throw new Error('OpenAI must be initialized'); | ||
} | ||
if(!prompt) { | ||
throw new Error('Prompt must be a string'); | ||
} | ||
const defaultOptions = { | ||
model: OPENAI_MODEL, | ||
} | ||
const options = { | ||
...defaultOptions, | ||
...paramOptions, | ||
} | ||
log('prompt','debug',prompt); | ||
const chatCompletion = await openai.chat.completions.create({ | ||
messages: [ | ||
{ role: 'user', content: prompt }, | ||
{role: 'user', content: `Respond in ${LANGUAGE}`} | ||
], | ||
...options, | ||
}); | ||
if(chatCompletion.choices.length > 0 && chatCompletion.choices[0].message) { | ||
log('response','info',chatCompletion.choices[0].message.content); | ||
return chatCompletion.choices[0].message.content; | ||
} | ||
log(prompt, 'warn', 'No response from OpenAI API for prompt', prompt); | ||
return null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/** | ||
* Sleep for a given number of milliseconds. | ||
* @param ms - The number of milliseconds to sleep for. | ||
*/ | ||
export async function sleep(ms) { | ||
return new Promise(resolve => setTimeout(resolve, ms)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import {getEntities, addDomainToEntities, summarizeEntities} from '../_helpers/ha'; | ||
|
||
const mockStates = [ | ||
{ entity_id: 'binary_sensor.entry_door', attributes: { device_class: 'door'}, state: 'off' }, | ||
{ entity_id: 'binary_sensor.front_door', attributes: { device_class: 'door'}, state: 'on' }, | ||
{ entity_id: 'binary_sensor.living_window', attributes: { device_class: 'window'}, state: 'off' }, | ||
{ entity_id: 'light.living', state: 'off', attributes: {} }, | ||
{ entity_id: 'light.living', state: 'on', attributes: {}}, | ||
]; | ||
|
||
describe('getEntities', () => { | ||
it('should return all entities if no filter is applied', () => { | ||
const result = getEntities(mockStates); | ||
expect(result).toEqual(mockStates); | ||
}); | ||
|
||
it('should return an array of entities for a specific domain', () => { | ||
const result = getEntities(mockStates, [{key: 'domain', value: 'binary_sensor'}], 'entity_id'); | ||
expect(result).toEqual([ | ||
'binary_sensor.entry_door', | ||
'binary_sensor.front_door', | ||
'binary_sensor.living_window', | ||
]); | ||
}); | ||
it('should return an array of entities for a specific domain with their state', () => { | ||
const result = getEntities(mockStates, [{key: 'domain', value: 'binary_sensor'}], 'entity_id', true); | ||
expect(result).toEqual([ | ||
'binary_sensor.entry_door (off)', | ||
'binary_sensor.front_door (on)', | ||
'binary_sensor.living_window (off)', | ||
]); | ||
}); | ||
|
||
it('should return an array of entities with a specific state', () => { | ||
const result = getEntities(mockStates, [{key: 'state', value: 'on'}], 'entity_id'); | ||
expect(result).toEqual([ | ||
'binary_sensor.front_door', | ||
'light.living', | ||
]); | ||
}); | ||
|
||
it('should return an array of entities with a specific device_class', () => { | ||
const result = getEntities(mockStates, [{key: 'attributes.device_class', value: 'door'}], 'entity_id'); | ||
expect(result).toEqual([ | ||
'binary_sensor.entry_door', | ||
'binary_sensor.front_door', | ||
]); | ||
}); | ||
|
||
it('should return an array of entities with a specific device_class and state', () => { | ||
const result = getEntities(mockStates, [ {key: 'attributes.device_class', value: 'door'}, {key: 'state', value: 'on'}], 'entity_id'); | ||
expect(result).toEqual([ | ||
'binary_sensor.front_door', | ||
]); | ||
}); | ||
}); | ||
|
||
describe('addDomainToEntities', () => { | ||
it('should add domain property to each entity', () => { | ||
const states = [ | ||
{ entity_id: 'light.living_room', state: 'on', attributes: {} }, | ||
{ entity_id: 'switch.kitchen', state: 'off', attributes: {} }, | ||
]; | ||
const expected = [ | ||
{ entity_id: 'light.living_room', state: 'on', attributes: {}, domain: 'light' }, | ||
{ entity_id: 'switch.kitchen', state: 'off', attributes: {}, domain: 'switch' }, | ||
]; | ||
expect(addDomainToEntities(states)).toEqual(expected); | ||
}); | ||
}); | ||
|
||
describe('summarizeEntities', () => { | ||
it('should return an array of strings summarizing the state of each entity', () => { | ||
const states = [ | ||
{ entity_id: 'light.living_room', state: 'on', attributes: {} }, | ||
{ entity_id: 'switch.kitchen', state: 'off', attributes: {friendly_name: 'Kitchen Switch'} }, | ||
]; | ||
const expected = [ | ||
'light.living_room: on ()', | ||
'switch.kitchen: off (friendly_name: Kitchen Switch)', | ||
]; | ||
expect(summarizeEntities(states)).toEqual(expected); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,43 @@ | ||
import 'dotenv/config'; | ||
import {init} from './init'; | ||
|
||
import {log} from './_helpers/logs'; | ||
import {sleep} from './_helpers/sleep'; | ||
|
||
import { | ||
SUMMARY_DEVICES_ENTITIES, | ||
UPDATE_INTERVAL, | ||
} from './config'; | ||
import {init} from './init'; | ||
import {updateDeviceSummary} from './summary' | ||
|
||
async function main() { | ||
|
||
// Init | ||
// =================================================================================================================== | ||
log('app','info','Starting up...'); | ||
log('app','info','Here are your environment variables:'); | ||
log('app','info',process.env); | ||
|
||
const {ha, openai } = await init(); | ||
log('app','info','Initialized.'); | ||
|
||
|
||
// Jobs | ||
// =================================================================================================================== | ||
const states = await ha.states.list(); | ||
|
||
while(true) { | ||
|
||
// Device Summaries | ||
// =================================================================================================================== | ||
log('app','info',`Updating summary for ${SUMMARY_DEVICES_ENTITIES.length} devices...`); | ||
await Promise.allSettled(SUMMARY_DEVICES_ENTITIES.map(device => updateDeviceSummary(openai, ha, states, device))); | ||
log('app','info',`Summary updated for ${SUMMARY_DEVICES_ENTITIES.length} devices.`); | ||
|
||
// Sleep till next run | ||
// =================================================================================================================== | ||
// TODO: Implement publish/subscribe pattern to avoid polling | ||
log('app','info',`Sleeping for ${UPDATE_INTERVAL/1000} seconds...`); | ||
await sleep(UPDATE_INTERVAL); | ||
|
||
} | ||
} | ||
|
||
main(); |
Oops, something went wrong.