Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for Jinja templates #218

Merged
merged 4 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,38 @@ After you save your Home Assistant Settings, the plugin will automatically try t
"rgb_color": [255, 0, 0]
}
```
You can also use [Jinja2 templates](https://www.home-assistant.io/docs/configuration/templating/) to dynamically process data based on the states or attributes of your Home Assistant entities. See [Using Jinja2 Templates in Service Data JSON](#Advanced-configuration) for more information.

![img.png](doc/entity_settings.png)

### Advanced configuration
* Custom Title: Enable to override the main Title of this button. **You have to clear the main Title field on top to make this work!**
* **Custom Title:** Enable to override the main Title of this button. **You have to clear the main Title field on top to make this work!**
* Title Template: A [nunjucks template](https://mozilla.github.io/nunjucks/templating.html) that will be used as the button's title. You can use any of the variables (depending on the selected entity) that are shown below the text-field. For example `{{temperature}}°C` or `{{friendly_name}}` or (this won't fit the button, but you get the idea) `The pressure is {{pressure}} and the wind speed is {{wind_speed}}.`
* The variable `{{state}}` always contains the "main state" of an entity (for example "on" and "off" for buttons or "12.4" for temperature sensors)
* The variable `{{unit_of_measurement}}` often contains the ... unit of measurement ... of a sensor's state (for example "°C" for a temperature sensor)

* Custom Labels: Every button can display up to 4 lines of information
* **Custom Labels:** Every button can display up to 4 lines of information
* Each line in the text-box represents one line on the button
* Depending on if there is an icon or a title for the entity, you may need to leave blank lines in order to not mess up the layout :)
* You can use [nunjucks template](https://mozilla.github.io/nunjucks/templating.html) for dynamic content (see above).

After you hit the save button, the button should immediately show the new configuration.

![img.png](doc/custom_labels.png)
After you hit the save button, the button should immediately show the new configuration.

![img.png](doc/custom_labels.png)

* **Using Jinja2 Templates in Service Data JSON**
* **Jinja2 Template Integration:** You can incorporate Jinja2 templates within the Service Data JSON to dynamically
process data based on the states or attributes of your Home Assistant entities.
* **Encapsulation with Raw Tags:** It's crucial to enclose Jinja2 templates within `{% raw %}` and `{% endraw %}`
tags. This encapsulation ensures that the StreamDeck plugin processes these templates as Jinja2, distinct from any
Nunjucks templates you might use elsewhere in your configurations.
* **Example of Jinja2 Template Usage:**

```json
{
"temperature": "{% raw %}{{ state_attr('climate.ff_office_heating','temperature') + 0.5 }}{% endraw %}"
}
```

# Happy? Consider donating me a coffee :)
[![buy me a coffee](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/donate?hosted_button_id=3UKRJEJVWV9H4)
Expand Down
15 changes: 15 additions & 0 deletions src/modules/homeassistant/actions/action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Abstract base class for actions. It serves as a foundation for all specific action types and is not
* intended for direct instantiation.
*/
export class Action {
/**
* Constructs an Action instance. Blocks direct instantiation of this abstract class.
* @throws {TypeError} If directly instantiated.
*/
constructor() {
if (new.target === Action) {
throw new TypeError('Cannot instantiate abstract class Action directly')
}
}
}
36 changes: 36 additions & 0 deletions src/modules/homeassistant/actions/service-action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Action } from '@/modules/homeassistant/actions/action'

/**
* ServiceAction, extending Action, facilitates interactions with HomeAssistant services.
*/
export class ServiceAction extends Action {
/**
* Constructs a ServiceAction instance.
* @param {string} domain - Service domain, must be a non-empty string.
* @param {string} service - Service name, must be a non-empty string.
* @param {Array} [entity_id=[]] - Target entity IDs array.
* @param {Object} [serviceData={}] - Additional service data.
* @throws {Error} if 'domain' or 'service' are empty or not strings.
* @throws {TypeError} if 'entity_id' is not an array or 'serviceData' is not an object.
*/
constructor(domain, service, entity_id, serviceData) {
super()

if (typeof domain !== 'string' || !domain.trim()) {
throw new Error('Domain must be a non-empty string')
}
if (typeof service !== 'string' || !service.trim()) {
throw new Error('Service must be a non-empty string')
}
if (!Array.isArray(entity_id)) {
throw new TypeError('entity_id must be an array')
}
if (typeof serviceData !== 'object' || serviceData === null) {
throw new TypeError('serviceData must be an object')
}

this.service = `${domain}.${service}`
this.data = serviceData
this.target = { entity_id: entity_id }
}
}
31 changes: 31 additions & 0 deletions src/modules/homeassistant/commands/command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* The Command class acts as an abstract base class for creating commands
* that can be used to interact with the HomeAssistant WebSocket API.
*/
export class Command {
/**
* Constructs a Command instance.
* @param {number} requestId - The unique identifier for the command request.
* @param {string} type - The type of the command. Must be a non-empty string.
* @throws {TypeError} If an attempt is made to instantiate Command directly.
* @throws {Error} If the requestId is not a non-negative number.
* @throws {Error} If the type is not a non-empty string.
*/
constructor(requestId, type) {
// Prevent direct instantiation of this abstract class.
if (new.target === Command) {
throw new TypeError('Cannot instantiate abstract class Command directly')
}

if (typeof requestId !== 'number' || requestId < 0) {
throw new Error('requestId must be a non-negative number')
}

if (typeof type !== 'string' || !type.trim()) {
throw new Error('type must be a non-empty string')
}

this.id = requestId
this.type = type
}
}
32 changes: 32 additions & 0 deletions src/modules/homeassistant/commands/execute-script-command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Command } from '@/modules/homeassistant/commands/command'
import { Action } from '@/modules/homeassistant/actions/action'

/**
* CallExecuteScriptCommand
*
* Facilitates the execution of multiple actions, including service calls, in a single command. This command
* is a substantial improvement over the "call_service" command, as it incorporates and evaluates Jinja templates.
* This enhancement enables more dynamic and context-sensitive operations within HomeAssistant.
*/
export class ExecuteScriptCommand extends Command {
/**
* Constructs a CallExecuteScriptCommand instance.
*
* @param {number} requestId - Number of iterations for execution. Must be non-negative.
* @param {Action[]} [actions=[]] - Array of ScriptCommand instances. Optional, defaults to empty.
* @throws {TypeError} if actions is not an array or has non-Action elements.
*/
constructor(requestId, actions = []) {
super(requestId, 'execute_script')

if (!Array.isArray(actions)) {
throw new TypeError('Actions must be an array')
}

if (actions.some((action) => !(action instanceof Action))) {
throw new TypeError('Elements in actions must be Action instances or subclasses')
}

this.sequence = actions
}
}
15 changes: 15 additions & 0 deletions src/modules/homeassistant/commands/get-services-command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Command } from '@/modules/homeassistant/commands/command'

/**
* The GetServicesCommand class, a subclass of Command, is used for requesting
* service information from HomeAssistant.
*/
export class GetServicesCommand extends Command {
/**
* Constructs a GetServicesCommand instance.*
* @param {number} requestId - The unique identifier for the command request.
*/
constructor(requestId) {
super(requestId, 'get_services')
}
}
15 changes: 15 additions & 0 deletions src/modules/homeassistant/commands/get-states-command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Command } from '@/modules/homeassistant/commands/command'

/**
* The GetStatesCommand class, a subclass of Command, handles the retrieval of
* state information from HomeAssistant.
*/
export class GetStatesCommand extends Command {
/**
* Constructs a GetStatesCommand instance.
* @param {number} requestId - The unique identifier for the command request.
*/
constructor(requestId) {
super(requestId, 'get_states')
}
}
16 changes: 16 additions & 0 deletions src/modules/homeassistant/commands/subscribe-events-command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Command } from '@/modules/homeassistant/commands/command'

/**
* The SubscribeEventCommand class, a subclass of Command, specifically handles
* subscription to event types in HomeAssistant.
*/
export class SubscribeEventsCommand extends Command {
/**
* Constructs a SubscribeEventCommand instance.
* @param {number} requestId - The unique identifier for the command request.
*/
constructor(requestId) {
super(requestId, 'subscribe_events')
this.event_type = 'state_changed'
}
}
Loading
Loading