mirror of
https://github.com/esphome/esphome.git
synced 2024-11-22 11:47:30 +01:00
Feature/m5angle8: Add support for m5angle8 input device (#6799)
Co-authored-by: Richard Nauber <richard@nauber.dev> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
368662969e
commit
40e79299d5
@ -216,6 +216,7 @@ esphome/components/lock/* @esphome/core
|
||||
esphome/components/logger/* @esphome/core
|
||||
esphome/components/ltr390/* @latonita @sjtrny
|
||||
esphome/components/ltr_als_ps/* @latonita
|
||||
esphome/components/m5stack_8angle/* @rnauber
|
||||
esphome/components/matrix_keypad/* @ssieb
|
||||
esphome/components/max31865/* @DAVe3283
|
||||
esphome/components/max44009/* @berfenger
|
||||
|
33
esphome/components/m5stack_8angle/__init__.py
Normal file
33
esphome/components/m5stack_8angle/__init__.py
Normal file
@ -0,0 +1,33 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
CODEOWNERS = ["@rnauber"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_M5STACK_8ANGLE_ID = "m5stack_8angle_id"
|
||||
|
||||
m5stack_8angle_ns = cg.esphome_ns.namespace("m5stack_8angle")
|
||||
M5Stack8AngleComponent = m5stack_8angle_ns.class_(
|
||||
"M5Stack8AngleComponent",
|
||||
i2c.I2CDevice,
|
||||
cg.Component,
|
||||
)
|
||||
|
||||
AnalogBits = m5stack_8angle_ns.enum("AnalogBits")
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(M5Stack8AngleComponent),
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x43))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
30
esphome/components/m5stack_8angle/binary_sensor/__init__.py
Normal file
30
esphome/components/m5stack_8angle/binary_sensor/__init__.py
Normal file
@ -0,0 +1,30 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import binary_sensor
|
||||
|
||||
from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID
|
||||
|
||||
|
||||
M5Stack8AngleSwitchBinarySensor = m5stack_8angle_ns.class_(
|
||||
"M5Stack8AngleSwitchBinarySensor",
|
||||
binary_sensor.BinarySensor,
|
||||
cg.PollingComponent,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
|
||||
}
|
||||
)
|
||||
.extend(binary_sensor.binary_sensor_schema(M5Stack8AngleSwitchBinarySensor))
|
||||
.extend(cv.polling_component_schema("10s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID])
|
||||
sens = await binary_sensor.new_binary_sensor(config)
|
||||
cg.add(sens.set_parent(hub))
|
||||
await cg.register_component(sens, config)
|
@ -0,0 +1,17 @@
|
||||
#include "m5stack_8angle_binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace m5stack_8angle {
|
||||
|
||||
void M5Stack8AngleSwitchBinarySensor::update() {
|
||||
int8_t out = this->parent_->read_switch();
|
||||
if (out == -1) {
|
||||
this->status_set_warning("Could not read binary sensor state from M5Stack 8Angle.");
|
||||
return;
|
||||
}
|
||||
this->publish_state(out != 0);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
} // namespace m5stack_8angle
|
||||
} // namespace esphome
|
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include "../m5stack_8angle.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace m5stack_8angle {
|
||||
|
||||
class M5Stack8AngleSwitchBinarySensor : public binary_sensor::BinarySensor,
|
||||
public PollingComponent,
|
||||
public Parented<M5Stack8AngleComponent> {
|
||||
public:
|
||||
void update() override;
|
||||
};
|
||||
|
||||
} // namespace m5stack_8angle
|
||||
} // namespace esphome
|
31
esphome/components/m5stack_8angle/light/__init__.py
Normal file
31
esphome/components/m5stack_8angle/light/__init__.py
Normal file
@ -0,0 +1,31 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import light
|
||||
|
||||
from esphome.const import CONF_OUTPUT_ID
|
||||
|
||||
from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID
|
||||
|
||||
|
||||
M5Stack8AngleLightsComponent = m5stack_8angle_ns.class_(
|
||||
"M5Stack8AngleLightOutput",
|
||||
light.AddressableLight,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(M5Stack8AngleLightsComponent),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID])
|
||||
lights = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
await light.register_light(lights, config)
|
||||
await cg.register_component(lights, config)
|
||||
cg.add(lights.set_parent(hub))
|
@ -0,0 +1,45 @@
|
||||
#include "m5stack_8angle_light.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace m5stack_8angle {
|
||||
|
||||
static const char *const TAG = "m5stack_8angle.light";
|
||||
|
||||
void M5Stack8AngleLightOutput::setup() {
|
||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->buf_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
|
||||
if (this->buf_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate buffer of size %u", M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
|
||||
this->mark_failed();
|
||||
return;
|
||||
};
|
||||
memset(this->buf_, 0xFF, M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
|
||||
|
||||
this->effect_data_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS);
|
||||
if (this->effect_data_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate effect data of size %u", M5STACK_8ANGLE_NUM_LEDS);
|
||||
this->mark_failed();
|
||||
return;
|
||||
};
|
||||
memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS);
|
||||
}
|
||||
|
||||
void M5Stack8AngleLightOutput::write_state(light::LightState *state) {
|
||||
for (int i = 0; i < M5STACK_8ANGLE_NUM_LEDS;
|
||||
i++) { // write one LED at a time, otherwise the message will be truncated
|
||||
this->parent_->write_register(M5STACK_8ANGLE_REGISTER_RGB_24B + i * M5STACK_8ANGLE_BYTES_PER_LED,
|
||||
this->buf_ + i * M5STACK_8ANGLE_BYTES_PER_LED, M5STACK_8ANGLE_BYTES_PER_LED);
|
||||
}
|
||||
}
|
||||
|
||||
light::ESPColorView M5Stack8AngleLightOutput::get_view_internal(int32_t index) const {
|
||||
size_t pos = index * M5STACK_8ANGLE_BYTES_PER_LED;
|
||||
// red, green, blue, white, effect_data, color_correction
|
||||
return {this->buf_ + pos, this->buf_ + pos + 1, this->buf_ + pos + 2,
|
||||
nullptr, this->effect_data_ + index, &this->correction_};
|
||||
}
|
||||
|
||||
} // namespace m5stack_8angle
|
||||
} // namespace esphome
|
@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/light/addressable_light.h"
|
||||
#include "esphome/components/light/light_output.h"
|
||||
|
||||
#include "../m5stack_8angle.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace m5stack_8angle {
|
||||
|
||||
static const uint8_t M5STACK_8ANGLE_NUM_LEDS = 9;
|
||||
static const uint8_t M5STACK_8ANGLE_BYTES_PER_LED = 4;
|
||||
|
||||
class M5Stack8AngleLightOutput : public light::AddressableLight, public Parented<M5Stack8AngleComponent> {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
void write_state(light::LightState *state) override;
|
||||
|
||||
int32_t size() const override { return M5STACK_8ANGLE_NUM_LEDS; }
|
||||
light::LightTraits get_traits() override {
|
||||
auto traits = light::LightTraits();
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||
return traits;
|
||||
};
|
||||
|
||||
void clear_effect_data() override { memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS); };
|
||||
|
||||
protected:
|
||||
light::ESPColorView get_view_internal(int32_t index) const override;
|
||||
|
||||
uint8_t *buf_{nullptr};
|
||||
uint8_t *effect_data_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace m5stack_8angle
|
||||
} // namespace esphome
|
74
esphome/components/m5stack_8angle/m5stack_8angle.cpp
Normal file
74
esphome/components/m5stack_8angle/m5stack_8angle.cpp
Normal file
@ -0,0 +1,74 @@
|
||||
#include "m5stack_8angle.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace m5stack_8angle {
|
||||
|
||||
static const char *const TAG = "m5stack_8angle";
|
||||
|
||||
void M5Stack8AngleComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up M5STACK_8ANGLE...");
|
||||
i2c::ErrorCode err;
|
||||
|
||||
err = this->read(nullptr, 0);
|
||||
if (err != i2c::NO_ERROR) {
|
||||
ESP_LOGE(TAG, "I2C error %02X...", err);
|
||||
this->mark_failed();
|
||||
return;
|
||||
};
|
||||
|
||||
err = this->read_register(M5STACK_8ANGLE_REGISTER_FW_VERSION, &this->fw_version_, 1);
|
||||
if (err != i2c::NO_ERROR) {
|
||||
ESP_LOGE(TAG, "I2C error %02X...", err);
|
||||
this->mark_failed();
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
void M5Stack8AngleComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "M5STACK_8ANGLE:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
ESP_LOGCONFIG(TAG, " Firmware version: %d ", this->fw_version_);
|
||||
}
|
||||
|
||||
float M5Stack8AngleComponent::read_knob_pos(uint8_t channel, AnalogBits bits) {
|
||||
int32_t raw_pos = this->read_knob_pos_raw(channel, bits);
|
||||
if (raw_pos == -1) {
|
||||
return NAN;
|
||||
}
|
||||
return (float) raw_pos / ((1 << bits) - 1);
|
||||
}
|
||||
|
||||
int32_t M5Stack8AngleComponent::read_knob_pos_raw(uint8_t channel, AnalogBits bits) {
|
||||
uint16_t knob_pos = 0;
|
||||
i2c::ErrorCode err;
|
||||
if (bits == BITS_8) {
|
||||
err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B + channel, (uint8_t *) &knob_pos, 1);
|
||||
} else if (bits == BITS_12) {
|
||||
err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B + (channel * 2), (uint8_t *) &knob_pos, 2);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid number of bits: %d", bits);
|
||||
return -1;
|
||||
}
|
||||
if (err == i2c::NO_ERROR) {
|
||||
return knob_pos;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int8_t M5Stack8AngleComponent::read_switch() {
|
||||
uint8_t out;
|
||||
i2c::ErrorCode err = this->read_register(M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT, (uint8_t *) &out, 1);
|
||||
if (err == i2c::NO_ERROR) {
|
||||
return out ? 1 : 0;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
float M5Stack8AngleComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace m5stack_8angle
|
||||
} // namespace esphome
|
34
esphome/components/m5stack_8angle/m5stack_8angle.h
Normal file
34
esphome/components/m5stack_8angle/m5stack_8angle.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace m5stack_8angle {
|
||||
|
||||
static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B = 0x00;
|
||||
static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B = 0x10;
|
||||
static const uint8_t M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT = 0x20;
|
||||
static const uint8_t M5STACK_8ANGLE_REGISTER_RGB_24B = 0x30;
|
||||
static const uint8_t M5STACK_8ANGLE_REGISTER_FW_VERSION = 0xFE;
|
||||
|
||||
enum AnalogBits : uint8_t {
|
||||
BITS_8 = 8,
|
||||
BITS_12 = 12,
|
||||
};
|
||||
|
||||
class M5Stack8AngleComponent : public i2c::I2CDevice, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
float read_knob_pos(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8);
|
||||
int32_t read_knob_pos_raw(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8);
|
||||
int8_t read_switch();
|
||||
|
||||
protected:
|
||||
uint8_t fw_version_;
|
||||
};
|
||||
|
||||
} // namespace m5stack_8angle
|
||||
} // namespace esphome
|
66
esphome/components/m5stack_8angle/sensor/__init__.py
Normal file
66
esphome/components/m5stack_8angle/sensor/__init__.py
Normal file
@ -0,0 +1,66 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
|
||||
from esphome.const import (
|
||||
CONF_BIT_DEPTH,
|
||||
CONF_CHANNEL,
|
||||
CONF_RAW,
|
||||
ICON_ROTATE_RIGHT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
from .. import (
|
||||
AnalogBits,
|
||||
M5Stack8AngleComponent,
|
||||
m5stack_8angle_ns,
|
||||
CONF_M5STACK_8ANGLE_ID,
|
||||
)
|
||||
|
||||
|
||||
M5Stack8AngleKnobSensor = m5stack_8angle_ns.class_(
|
||||
"M5Stack8AngleKnobSensor",
|
||||
sensor.Sensor,
|
||||
cg.PollingComponent,
|
||||
)
|
||||
|
||||
|
||||
BIT_DEPTHS = {
|
||||
8: AnalogBits.BITS_8,
|
||||
12: AnalogBits.BITS_12,
|
||||
}
|
||||
|
||||
_validate_bits = cv.float_with_unit("bits", "bit")
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(M5Stack8AngleKnobSensor),
|
||||
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=8),
|
||||
cv.Optional(CONF_BIT_DEPTH, default="8bit"): cv.All(
|
||||
_validate_bits, cv.enum(BIT_DEPTHS)
|
||||
),
|
||||
cv.Optional(CONF_RAW, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
sensor.sensor_schema(
|
||||
M5Stack8AngleKnobSensor,
|
||||
accuracy_decimals=2,
|
||||
icon=ICON_ROTATE_RIGHT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
)
|
||||
.extend(cv.polling_component_schema("10s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_M5STACK_8ANGLE_ID])
|
||||
cg.add(var.set_channel(config[CONF_CHANNEL] - 1))
|
||||
cg.add(var.set_bit_depth(BIT_DEPTHS[config[CONF_BIT_DEPTH]]))
|
||||
cg.add(var.set_raw(config[CONF_RAW]))
|
@ -0,0 +1,24 @@
|
||||
#include "m5stack_8angle_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace m5stack_8angle {
|
||||
|
||||
void M5Stack8AngleKnobSensor::update() {
|
||||
if (this->parent_ != nullptr) {
|
||||
int32_t raw_pos = this->parent_->read_knob_pos_raw(this->channel_, this->bits_);
|
||||
if (raw_pos == -1) {
|
||||
this->status_set_warning("Could not read knob position from M5Stack 8Angle.");
|
||||
return;
|
||||
}
|
||||
if (this->raw_) {
|
||||
this->publish_state(raw_pos);
|
||||
} else {
|
||||
float knob_pos = (float) raw_pos / ((1 << this->bits_) - 1);
|
||||
this->publish_state(knob_pos);
|
||||
}
|
||||
this->status_clear_warning();
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace m5stack_8angle
|
||||
} // namespace esphome
|
@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include "../m5stack_8angle.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace m5stack_8angle {
|
||||
|
||||
class M5Stack8AngleKnobSensor : public sensor::Sensor,
|
||||
public PollingComponent,
|
||||
public Parented<M5Stack8AngleComponent> {
|
||||
public:
|
||||
void update() override;
|
||||
void set_channel(uint8_t channel) { this->channel_ = channel; };
|
||||
void set_bit_depth(AnalogBits bits) { this->bits_ = bits; };
|
||||
void set_raw(bool raw) { this->raw_ = raw; };
|
||||
|
||||
protected:
|
||||
uint8_t channel_;
|
||||
AnalogBits bits_;
|
||||
bool raw_;
|
||||
};
|
||||
|
||||
} // namespace m5stack_8angle
|
||||
} // namespace esphome
|
30
tests/components/m5stack_8angle/common.yaml
Normal file
30
tests/components/m5stack_8angle/common.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
i2c:
|
||||
sda: 0
|
||||
scl: 1
|
||||
id: bus_external
|
||||
|
||||
m5stack_8angle:
|
||||
i2c_id: bus_external
|
||||
id: m5stack_8angle_base
|
||||
|
||||
light:
|
||||
- platform: m5stack_8angle
|
||||
m5stack_8angle_id: m5stack_8angle_base
|
||||
id: m8_angle_leds
|
||||
name: Lights
|
||||
effects:
|
||||
- addressable_scan:
|
||||
|
||||
sensor:
|
||||
- platform: m5stack_8angle
|
||||
m5stack_8angle_id: m5stack_8angle_base
|
||||
channel: 1
|
||||
name: Knob 1
|
||||
- platform: m5stack_8angle
|
||||
m5stack_8angle_id: m5stack_8angle_base
|
||||
channel: 2
|
||||
name: Knob 2
|
||||
binary_sensor:
|
||||
- platform: m5stack_8angle
|
||||
m5stack_8angle_id: m5stack_8angle_base
|
||||
name: Switch
|
1
tests/components/m5stack_8angle/test.esp32-ard.yaml
Normal file
1
tests/components/m5stack_8angle/test.esp32-ard.yaml
Normal file
@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
1
tests/components/m5stack_8angle/test.esp32-c3-ard.yaml
Normal file
1
tests/components/m5stack_8angle/test.esp32-c3-ard.yaml
Normal file
@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
1
tests/components/m5stack_8angle/test.esp32-c3-idf.yaml
Normal file
1
tests/components/m5stack_8angle/test.esp32-c3-idf.yaml
Normal file
@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
1
tests/components/m5stack_8angle/test.esp32-idf.yaml
Normal file
1
tests/components/m5stack_8angle/test.esp32-idf.yaml
Normal file
@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
1
tests/components/m5stack_8angle/test.esp8266-ard.yaml
Normal file
1
tests/components/m5stack_8angle/test.esp8266-ard.yaml
Normal file
@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
1
tests/components/m5stack_8angle/test.rp2040-ard.yaml
Normal file
1
tests/components/m5stack_8angle/test.rp2040-ard.yaml
Normal file
@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
Loading…
Reference in New Issue
Block a user