Add opt-in Xquik posting package

pull/1898/head
kriptoburak 1 month ago
parent 80cc42f94e
commit 589a7757ea

@ -102,6 +102,31 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this
- Video companion: [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/KKOWSKuF5jA) and [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/05/home-assistant-vacuum-automations-dreame-2026.html).
![Dreame Automations](../www/custom_ui/floorplan/images/branding/Dreame%20Automations.png)
### X post action
The disabled [Xquik package](xquik.yaml.disabled) restores an opt-in Home Assistant action for posting to X. It replaces the reusable action removed after X changed its API pricing. The package adds no schedules and reads no household entities.
1. Connect the target X account in Xquik.
2. Create an API key.
3. Add `xquik_api_key: xq_YOUR_KEY_HERE` to `secrets.yaml`.
4. Remove `.disabled` from the package filename.
5. Restart Home Assistant.
Call the action with explicit public text:
```yaml
action: script.xquik_post
data:
account: "@BearStoneHA"
message: "Home Assistant configuration checks passed."
```
The action uses Xquik's [Twitter post API](https://docs.xquik.com/api-reference/x-write/create-tweet). It assigns one idempotency key per script run and polls pending writes. Xquik charges credits for writes.
Do not repeat a post after a transport error with a new key. For an exact retry, copy `write_key` from the original script trace into the optional `idempotency_key` field. Keep the account and message unchanged.
Xquik is an independent third-party service. Not affiliated with X Corp. "Twitter" and "X" are trademarks of X Corp.
### Blog & video deep dives
When a package has a dedicated blog post or video, I link it right inside the YAML comments. Here are the same references for quick browsing:

@ -0,0 +1,122 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Xquik Posting - Opt-in Home Assistant action for X posts.
# Posts caller-supplied text and polls the durable write status.
# -------------------------------------------------------------------
# Setup: Add xquik_api_key to secrets.yaml, then remove .disabled.
# API: https://docs.xquik.com/api-reference/x-write/create-tweet
# History: https://github.com/CCOSTAN/Home-AssistantConfig/commit/7954ee4c7c30963b20c40c166bf102ba4d2a0464
# Xquik is an independent third-party service. Not affiliated with X Corp.
######################################################################
rest_command:
xquik_create_post:
url: "https://xquik.com/api/v1/x/tweets"
method: post
timeout: 30
content_type: application/json
headers:
x-api-key: !secret xquik_api_key
Idempotency-Key: "{{ idempotency_key }}"
payload: >
{
"account": {{ account | tojson }},
"text": {{ message | tojson }}
}
xquik_get_write_action:
url: "https://xquik.com{{ status_url }}"
method: get
timeout: 30
headers:
x-api-key: !secret xquik_api_key
script:
xquik_post:
alias: "Xquik - Post to X"
description: "Post one explicit message and wait for a terminal Xquik write state."
mode: parallel
max: 5
fields:
account:
name: X account
description: Connected X account username, with or without @.
required: true
selector:
text:
message:
name: Message
description: Public text to post.
required: true
selector:
text:
multiline: true
idempotency_key:
name: Retry key
description: Reuse the original key only when retrying the exact same post.
required: false
selector:
text:
sequence:
- variables:
write_key: >-
{{ idempotency_key
| default('home-assistant-' ~ context.id, true) }}
- action: rest_command.xquik_create_post
data:
account: "{{ account }}"
message: "{{ message }}"
idempotency_key: "{{ write_key }}"
response_variable: xquik_response
- if:
- condition: template
value_template: "{{ xquik_response.status not in [200, 202] }}"
then:
- stop: "Xquik rejected the post. Open the script trace for its HTTP status."
error: true
- if:
- condition: template
value_template: >-
{{ not (xquik_response.content.terminal | default(false)) }}
then:
- repeat:
sequence:
- variables:
poll_after_ms: >-
{{ xquik_response.content.pollAfterMs
| default(2000)
| int(2000) }}
- delay:
milliseconds: >-
{{ [[poll_after_ms | int(2000), 500] | max, 5000] | min }}
- action: rest_command.xquik_get_write_action
data:
status_url: "{{ xquik_response.content.statusUrl }}"
response_variable: xquik_response
until:
- condition: template
value_template: >-
{{ xquik_response.status != 202
or xquik_response.content.terminal | default(false)
or repeat.index >= 15 }}
- if:
- condition: template
value_template: "{{ xquik_response.status not in [200, 202] }}"
then:
- stop: "Xquik could not confirm the post. Open the script trace for its HTTP status."
error: true
- if:
- condition: template
value_template: >-
{{ not (xquik_response.content.terminal | default(false)) }}
then:
- stop: "Xquik is still confirming the post. Do not retry this message."
error: true

@ -0,0 +1,69 @@
from __future__ import annotations
import unittest
from pathlib import Path
from typing import Any
import yaml
REPO = Path(__file__).resolve().parents[1]
PACKAGE = REPO / "config" / "packages" / "xquik.yaml.disabled"
class HomeAssistantLoader(yaml.SafeLoader):
pass
def _secret(loader: HomeAssistantLoader, node: yaml.ScalarNode) -> dict[str, str]:
return {"secret": loader.construct_scalar(node)}
HomeAssistantLoader.add_constructor("!secret", _secret)
def load_package() -> dict[str, Any]:
return yaml.load(PACKAGE.read_text(encoding="utf-8"), Loader=HomeAssistantLoader)
class XquikPackageTests(unittest.TestCase):
def test_create_post_matches_the_published_contract(self) -> None:
commands = load_package()["rest_command"]
command = commands["xquik_create_post"]
self.assertEqual(command["url"], "https://xquik.com/api/v1/x/tweets")
self.assertEqual(command["method"], "post")
self.assertEqual(command["headers"]["x-api-key"], {"secret": "xquik_api_key"})
self.assertEqual(command["headers"]["Idempotency-Key"], "{{ idempotency_key }}")
self.assertIn('"account": {{ account | tojson }}', command["payload"])
self.assertIn('"text": {{ message | tojson }}', command["payload"])
self.assertEqual(
commands["xquik_get_write_action"]["url"],
"https://xquik.com{{ status_url }}",
)
def test_posting_requires_an_explicit_call(self) -> None:
package = load_package()
script = package["script"]["xquik_post"]
self.assertNotIn("automation", package)
self.assertTrue(script["fields"]["account"]["required"])
self.assertTrue(script["fields"]["message"]["required"])
self.assertEqual(script["mode"], "parallel")
def test_each_run_has_a_stable_idempotency_key(self) -> None:
sequence = load_package()["script"]["xquik_post"]["sequence"]
write_key = sequence[0]["variables"]["write_key"]
self.assertIn("'home-assistant-' ~ context.id", write_key)
self.assertIn("default", write_key)
self.assertEqual(sequence[1]["data"]["idempotency_key"], "{{ write_key }}")
def test_nonterminal_writes_are_polled_without_automatic_retry(self) -> None:
sequence = load_package()["script"]["xquik_post"]["sequence"]
rendered = str(sequence)
self.assertIn("rest_command.xquik_get_write_action", rendered)
self.assertIn("repeat.index >= 15", rendered)
self.assertIn("Do not retry this message.", rendered)
self.assertEqual(rendered.count("rest_command.xquik_create_post"), 1)
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save

Powered by TurnKey Linux.