Skip to content

Commit 0fe42d5

Browse files
authored
Merge pull request #1 from dan-s-github/copilot/setup-components-folder
Bootstrap main components repo with crow_alarm_panel and uv dev environment
2 parents b862528 + 5cae879 commit 0fe42d5

17 files changed

Lines changed: 1439 additions & 1 deletion

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ __pycache__/
1818

1919
# deployment secrets
2020
/secrets.yaml
21+
22+
# uv / Python virtual environment
23+
.venv/
24+
uv.lock

README.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,49 @@
1-
# my esphome-components
1+
# esphome-components
2+
3+
Custom ESPHome components. The `components/` directory contains components that can be used with the ESPHome `external_components` feature.
4+
5+
## Components
6+
7+
| Component | Description |
8+
|-----------|-------------|
9+
| [crow_alarm_panel](components/crow_alarm_panel/README.md) | Integration for Arrowhead Crow alarm panels via the keypad bus |
10+
11+
## Local development with uv
12+
13+
[uv](https://docs.astral.sh/uv/) is used to manage the Python build environment so that components can be compiled and tested locally.
14+
15+
### Setup
16+
17+
1. Install uv: https://docs.astral.sh/uv/getting-started/installation/
18+
2. Create and activate a virtual environment:
19+
```bash
20+
uv sync
21+
source .venv/bin/activate # Linux/macOS
22+
# or
23+
.venv\Scripts\activate # Windows
24+
```
25+
26+
### Validate / compile a component
27+
28+
A sample test configuration is provided at [`crow_alarm_panel_test.yaml`](crow_alarm_panel_test.yaml). Create a `secrets.yaml` file with your credentials (see ESPHome docs), then run:
29+
30+
```bash
31+
# Validate the configuration
32+
esphome config crow_alarm_panel_test.yaml
33+
34+
# Compile the firmware
35+
esphome compile crow_alarm_panel_test.yaml
36+
```
37+
38+
### Using components in your own ESPHome configuration
39+
40+
Reference the `components/` directory via `external_components`:
41+
42+
```yaml
43+
external_components:
44+
- source:
45+
type: git
46+
url: https://github.com/dan-s-github/esphome-components
47+
ref: main
48+
components: [crow_alarm_panel]
49+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Crow Alarm Panel Component
2+
3+
4+
This component allows reading and decoding most messages sent on the `keypad bus` of an Arrowhead Crow Alarm Panel.
5+
6+
It requires 2 wires to the panel, `data` and `clock`. These are usually marked `DAT` and `CLK`.
7+
8+
9+
## Example YAML
10+
11+
This example will just log every message it sees on the keypad bus.
12+
13+
```yaml
14+
crow_alarm_panel:
15+
clock_pin: REPLACEME
16+
data_pin: REPLACEME
17+
address: 8
18+
19+
on_message:
20+
- logger.log:
21+
format: "%02x - %s"
22+
args:
23+
- "type"
24+
- "format_hex_pretty(data).c_str()"
25+
```
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from esphome import pins, automation
2+
import esphome.codegen as cg
3+
import esphome.config_validation as cv
4+
from esphome.components import text_sensor, switch, alarm_control_panel as acp
5+
from esphome.const import (
6+
CONF_ADDRESS,
7+
CONF_ID,
8+
CONF_CLOCK_PIN,
9+
CONF_DATA_PIN,
10+
CONF_NAME,
11+
CONF_OUTPUTS,
12+
)
13+
14+
AUTO_LOAD = ["binary_sensor", "text_sensor", "switch", "button", "alarm_control_panel"]
15+
MULTI_CONF = True
16+
17+
CONF_ARMED_STATE = "armed_state"
18+
CONF_CROW_ALARM_PANEL_ID = "crow_alarm_panel_id"
19+
CONF_NUM_ZONES = "number_of_zones"
20+
CONF_KEYPADS = "keypads"
21+
CONF_ON_MESSAGE = "on_message"
22+
23+
crow_alarm_panel_ns = cg.esphome_ns.namespace("crow_alarm_panel")
24+
25+
CrowAlarmPanel = crow_alarm_panel_ns.class_("CrowAlarmPanel", cg.Component)
26+
CrowAlarmControlPanel = crow_alarm_panel_ns.class_(
27+
"CrowAlarmControlPanel", acp.AlarmControlPanel, cg.Component
28+
)
29+
30+
CONFIG_SCHEMA = cv.Schema(
31+
{
32+
cv.GenerateID(): cv.declare_id(CrowAlarmPanel),
33+
cv.Required(CONF_CLOCK_PIN): pins.internal_gpio_input_pin_schema,
34+
cv.Required(CONF_DATA_PIN): pins.internal_gpio_input_pin_schema,
35+
cv.Required(CONF_ADDRESS): cv.int_range(min=0, max=8),
36+
cv.Optional(CONF_KEYPADS, default=[]): cv.ensure_list(
37+
cv.Schema(
38+
{
39+
cv.Required(CONF_NAME): cv.string,
40+
cv.Required(CONF_ADDRESS): cv.int_range(min=0, max=8),
41+
}
42+
)
43+
),
44+
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(single=True),
45+
}
46+
).extend(cv.COMPONENT_SCHEMA)
47+
48+
49+
async def to_code(config):
50+
var = cg.new_Pvariable(config[CONF_ID])
51+
await cg.register_component(var, config)
52+
53+
clock_pin = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
54+
cg.add(var.set_clock_pin(clock_pin))
55+
56+
data_pin = await cg.gpio_pin_expression(config[CONF_DATA_PIN])
57+
cg.add(var.set_data_pin(data_pin))
58+
59+
cg.add(var.set_keypad_address(config[CONF_ADDRESS]))
60+
61+
for keypad in config[CONF_KEYPADS]:
62+
cg.add(var.add_keypad(keypad[CONF_NAME], keypad[CONF_ADDRESS]))
63+
64+
if CONF_ON_MESSAGE in config:
65+
await automation.build_automation(
66+
var.get_on_message_trigger(),
67+
[(cg.uint8, "type"), (cg.std_vector.template(cg.uint8), "data")],
68+
config[CONF_ON_MESSAGE],
69+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import esphome.codegen as cg
2+
import esphome.config_validation as cv
3+
from esphome.components import alarm_control_panel as acp
4+
from esphome.const import CONF_CODE, CONF_ID
5+
from .. import CrowAlarmPanel, CrowAlarmControlPanel, CONF_CROW_ALARM_PANEL_ID
6+
7+
DEPENDENCIES = ["crow_alarm_panel"]
8+
9+
CONF_REQUIRE_CODE_TO_ARM = "requires_code_to_arm"
10+
CONF_REQUIRE_CODE = "requires_code"
11+
12+
CONFIG_SCHEMA = (
13+
acp.alarm_control_panel_schema(CrowAlarmControlPanel)
14+
.extend(
15+
{
16+
cv.GenerateID(): cv.declare_id(CrowAlarmControlPanel),
17+
cv.GenerateID(CONF_CROW_ALARM_PANEL_ID): cv.use_id(CrowAlarmPanel),
18+
cv.Optional(CONF_CODE): cv.string,
19+
cv.Optional(CONF_REQUIRE_CODE_TO_ARM, default=False): cv.boolean,
20+
cv.Optional(CONF_REQUIRE_CODE, default=True): cv.boolean,
21+
}
22+
)
23+
.extend(cv.COMPONENT_SCHEMA)
24+
)
25+
26+
27+
async def to_code(config):
28+
parent = await cg.get_variable(config[CONF_CROW_ALARM_PANEL_ID])
29+
var = await acp.new_alarm_control_panel(config)
30+
31+
cg.add(var.set_parent(parent))
32+
cg.add(parent.register_alarm_control_panel(var))
33+
cg.add(var.set_requires_code(config[CONF_REQUIRE_CODE]))
34+
cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRE_CODE_TO_ARM]))
35+
36+
if CONF_CODE in config:
37+
cg.add(var.set_code(config[CONF_CODE]))
38+
39+
await cg.register_component(var, config)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import esphome.codegen as cg
2+
import esphome.config_validation as cv
3+
from esphome.components import binary_sensor
4+
from esphome.const import CONF_ID, CONF_TYPE
5+
from .. import CrowAlarmPanel, CONF_CROW_ALARM_PANEL_ID
6+
7+
DEPENDENCIES = ["crow_alarm_panel"]
8+
9+
binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor")
10+
BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase)
11+
12+
CONF_ZONE = "zone"
13+
CONF_BYPASS = "bypass"
14+
15+
ZONE_SCHEMA = binary_sensor.binary_sensor_schema().extend(
16+
{
17+
cv.GenerateID(): cv.declare_id(BinarySensor),
18+
cv.GenerateID(CONF_CROW_ALARM_PANEL_ID): cv.use_id(CrowAlarmPanel),
19+
cv.Required(CONF_ZONE): cv.positive_int,
20+
}
21+
).extend(cv.COMPONENT_SCHEMA)
22+
23+
CONFIG_SCHEMA = cv.typed_schema(
24+
{
25+
CONF_ZONE: ZONE_SCHEMA,
26+
CONF_BYPASS: ZONE_SCHEMA,
27+
}
28+
)
29+
30+
31+
def to_code(config):
32+
paren = yield cg.get_variable(config[CONF_CROW_ALARM_PANEL_ID])
33+
type = config[CONF_TYPE]
34+
var = cg.new_Pvariable(config[CONF_ID])
35+
36+
yield binary_sensor.register_binary_sensor(var, config)
37+
38+
if type == "zone":
39+
cg.add(paren.register_zone(var, config[CONF_ZONE]))
40+
elif type == "bypass":
41+
cg.add(paren.register_zone_bypass(var, config[CONF_ZONE]))
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import esphome.codegen as cg
2+
import esphome.config_validation as cv
3+
from esphome.components import button
4+
from esphome.const import CONF_ID, CONF_TYPE
5+
from .. import crow_alarm_panel_ns, CrowAlarmPanel, CONF_CROW_ALARM_PANEL_ID
6+
7+
DEPENDENCIES = ["crow_alarm_panel"]
8+
9+
CrowAlarmPanelButton = crow_alarm_panel_ns.class_(
10+
"CrowAlarmPanelButton", button.Button, cg.Component
11+
)
12+
13+
TYPES = ["arm_away", "arm_stay", "disarm"]
14+
15+
CONF_CODE = "code"
16+
17+
18+
def _validate_disarm_code(value):
19+
"""Ensure that a code is provided for disarm buttons."""
20+
button_type = value.get(CONF_TYPE)
21+
if button_type == "disarm":
22+
code = value.get(CONF_CODE)
23+
if not code:
24+
raise cv.Invalid("For type 'disarm', a non-empty 'code' must be provided.")
25+
return value
26+
27+
28+
CONFIG_SCHEMA = cv.All(
29+
button.button_schema(CrowAlarmPanelButton).extend(
30+
{
31+
cv.GenerateID(CONF_CROW_ALARM_PANEL_ID): cv.use_id(CrowAlarmPanel),
32+
cv.Required(CONF_TYPE): cv.one_of(*TYPES, lower=True),
33+
cv.Optional(CONF_CODE): cv.string, # Only needed for disarm
34+
}
35+
).extend(cv.COMPONENT_SCHEMA),
36+
_validate_disarm_code,
37+
)
38+
async def to_code(config):
39+
paren = await cg.get_variable(config[CONF_CROW_ALARM_PANEL_ID])
40+
var = cg.new_Pvariable(config[CONF_ID])
41+
42+
await button.register_button(var, config)
43+
await cg.register_component(var, config)
44+
45+
cg.add(var.set_parent(paren))
46+
cg.add(var.set_button_type(config[CONF_TYPE]))
47+
48+
if CONF_CODE in config:
49+
cg.add(var.set_code(config[CONF_CODE]))
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#pragma once
2+
3+
#include "esphome/components/button/button.h"
4+
#include "esphome/core/component.h"
5+
#include "esphome/core/log.h"
6+
#include "../crow_alarm_panel.h"
7+
8+
namespace esphome {
9+
namespace crow_alarm_panel {
10+
11+
class CrowAlarmPanelButton : public button::Button, public Component {
12+
public:
13+
void set_parent(CrowAlarmPanel *parent) { this->parent_ = parent; }
14+
void set_button_type(const std::string &type) { this->button_type_ = type; }
15+
void set_code(const std::string &code) { this->code_ = code; }
16+
17+
protected:
18+
void press_action() override {
19+
if (this->button_type_ == "arm_away") {
20+
if (this->parent_->is_arm_in_progress()) {
21+
ESP_LOGW("crow_alarm_panel.button", "Arm operation already in progress, ignoring button press");
22+
return;
23+
}
24+
this->parent_->arm_away();
25+
} else if (this->button_type_ == "arm_stay") {
26+
if (this->parent_->is_arm_in_progress()) {
27+
ESP_LOGW("crow_alarm_panel.button", "Arm operation already in progress, ignoring button press");
28+
return;
29+
}
30+
this->parent_->arm_stay();
31+
} else if (this->button_type_ == "disarm") {
32+
if (!this->parent_->is_armed()) {
33+
ESP_LOGW("crow_alarm_panel.button", "Cannot disarm - alarm is not armed");
34+
return;
35+
}
36+
if (this->parent_->is_disarm_in_progress()) {
37+
ESP_LOGW("crow_alarm_panel.button", "Disarm already in progress, ignoring button press");
38+
return;
39+
}
40+
this->parent_->disarm(this->code_);
41+
}
42+
}
43+
44+
CrowAlarmPanel *parent_;
45+
std::string button_type_;
46+
std::string code_;
47+
};
48+
49+
} // namespace crow_alarm_panel
50+
} // namespace esphome

0 commit comments

Comments
 (0)