├── .github ├── CODEOWNERS ├── FUNDING.yml └── workflows │ ├── docs.yml │ ├── example_build.yml │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd └── zigbee │ ├── firmware │ ├── build.go │ ├── flash.go │ ├── flasher.go │ ├── flasher │ │ ├── adafruit_nrfutil.go │ │ ├── mcuboot.go │ │ ├── nrfutil.go │ │ └── west.go │ └── root.go │ └── main.go ├── config ├── device.go ├── device_test.go ├── ncs_finder.go └── tag_resolver.go ├── docs ├── features │ └── factory_reset.md ├── img │ ├── build.png │ ├── create_new_build_config.png │ └── new_build_config_params.png ├── index.md ├── install_zigbee_home_cli.md ├── sensors │ ├── supported_sensors.md │ └── supporting_new_sensors.md ├── supported_boards.md └── using_the_cli │ ├── building_the_firmware.md │ ├── configuration_file.md │ ├── flash_firmware.md │ ├── generating_source_code.md │ └── index.md ├── examples ├── README.md ├── adafruit_bootloader │ ├── README.md │ └── zigbee.yaml ├── basic │ ├── README.md │ └── zigbee.yaml ├── battery_measurement │ ├── README.md │ └── zigbee.yaml ├── config_tags │ ├── README.md │ ├── board.yaml │ └── zigbee.yaml ├── contact │ ├── README.md │ └── zigbee.yaml ├── custom_board │ ├── boards │ │ └── arm │ │ │ └── nice_nano │ │ │ ├── Kconfig │ │ │ ├── Kconfig.board │ │ │ ├── Kconfig.defconfig │ │ │ ├── arduino_pro_micro_pins.dtsi │ │ │ ├── board.cmake │ │ │ ├── nice_nano-pinctrl.dtsi │ │ │ ├── nice_nano.dts │ │ │ ├── nice_nano.dtsi │ │ │ ├── nice_nano.yaml │ │ │ ├── nice_nano.zmk.yml │ │ │ ├── nice_nano_defconfig │ │ │ ├── nice_nano_v2.dts │ │ │ ├── nice_nano_v2.yaml │ │ │ ├── nice_nano_v2.zmk.yml │ │ │ ├── nice_nano_v2_defconfig │ │ │ └── pre_dt_board.cmake │ └── zigbee.yaml ├── debug │ ├── README.md │ └── zigbee.yaml ├── examples_test.go ├── factory_reset │ ├── README.md │ └── zigbee.yaml ├── sensor_bme280 │ ├── README.md │ └── zigbee.yaml ├── sensor_dht │ ├── README.md │ └── zigbee.yaml ├── sensor_scd4x │ ├── README.md │ └── zigbee.yaml └── set_toolchain_version │ ├── README.md │ └── zigbee.yaml ├── generate └── generator.go ├── go.mod ├── go.sum ├── mkdocs.yml ├── runner └── cmd.go ├── sensor ├── aosong │ └── dht.go ├── base │ ├── base.go │ ├── common.go │ ├── ias_zone.go │ ├── on_off.go │ ├── power_config.go │ ├── soil_moisture_adc.go │ └── types.go ├── bosch │ ├── bme280.go │ └── bme680.go ├── device_temperature.go └── sensirion │ └── scd4x.go ├── templates ├── extenders │ ├── adc.go │ ├── bootloader.go │ ├── buttons.go │ ├── debug_log.go │ ├── gpio.go │ ├── i2c.go │ ├── leds.go │ ├── nrfx_temp.go │ ├── scd4x.go │ ├── sensor.go │ ├── uart.go │ └── usb.go ├── src │ ├── CMakeLists.txt.tpl │ ├── Kconfig.tpl │ ├── device.hpp.tpl │ ├── extenders │ │ ├── adc.c.tpl │ │ ├── adc.h.tpl │ │ ├── peripherals │ │ │ ├── adc.tpl │ │ │ ├── buttons.tpl │ │ │ └── leds.tpl │ │ ├── pm_static.yml.tpl │ │ ├── sensors │ │ │ ├── common.tpl │ │ │ ├── device_temperature.tpl │ │ │ ├── ias_zone.tpl │ │ │ ├── on_off.tpl │ │ │ ├── power_config.tpl │ │ │ └── soil_moisture_adc.tpl │ │ ├── zbhome_sensor.cpp.tpl │ │ └── zbhome_sensor.hpp.tpl │ ├── main.cpp.tpl │ ├── modules │ │ └── scd4x │ │ │ ├── dts │ │ │ └── bindings │ │ │ │ └── sensor │ │ │ │ └── sensirion,scd4x.yaml │ │ │ └── zephyr │ │ │ ├── CMakeLists.txt │ │ │ ├── Kconfig │ │ │ ├── module.yml │ │ │ ├── scd4x.c │ │ │ └── scd4x.h │ └── zigbee │ │ ├── attribute_setter.h.tpl │ │ ├── attributes.c.tpl │ │ ├── basic.c.tpl │ │ ├── carbon_dioxide.tpl │ │ ├── clusters.hpp.tpl │ │ ├── device_ctx.c.tpl │ │ ├── device_temperature.c.tpl │ │ ├── ias_zone.c.tpl │ │ ├── identify.c.tpl │ │ ├── on_off.c.tpl │ │ ├── power_config.c.tpl │ │ ├── pressure.c.tpl │ │ ├── temperature.c.tpl │ │ └── water_content.tpl ├── template_tree.go └── templates.go ├── types ├── appconfig │ ├── appconfig.go │ ├── doc.go │ └── known.go ├── board │ └── known_boards.go ├── devicetree │ ├── adc_pin.go │ ├── devicetree.go │ ├── devicetree_test.go │ ├── doc.go │ ├── known.go │ └── property.go ├── generator │ └── generator.go ├── option.go ├── pin.go ├── pin_test.go ├── semver.go ├── semver_test.go ├── sensor │ ├── known.go │ ├── sensor.go │ ├── sensor_test.go │ └── simple.go ├── source │ └── source.go ├── yamlstrict │ └── unmarshal.go └── zigbee.go ├── zboss_license.txt ├── zcl └── cluster │ ├── basic.go │ ├── carbon_dioxide.go │ ├── cluster_ids.go │ ├── common.go │ ├── device_temperature.go │ ├── ias_zone.go │ ├── identify.go │ ├── on_off.go │ ├── power_config.go │ ├── pressure.go │ ├── temperature.go │ └── water_content.go └── zigbee.yaml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /.github/ @ffenix113 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ffenix113 4 | buy_me_a_coffee: flayer151 -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - develop # develop is current source of truth 7 | paths: 8 | - 'docs/**' # Only run for docs directory & config change 9 | - 'mkdocs.yml' 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Configure Git Credentials 16 | run: | 17 | git config user.name github-actions[bot] 18 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 19 | - uses: actions/setup-python@v4 20 | with: 21 | python-version: 3.x 22 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 23 | - uses: actions/cache@v3 24 | with: 25 | key: mkdocs-material-${{ env.cache_id }} 26 | path: .cache 27 | restore-keys: | 28 | mkdocs-material- 29 | - run: pip install mkdocs-material 30 | - run: mkdocs gh-deploy --strict --force 31 | -------------------------------------------------------------------------------- /.github/workflows/example_build.yml: -------------------------------------------------------------------------------- 1 | name: Prepared SDK env 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.head_ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | empty: 12 | runs-on: self-hosted 13 | steps: 14 | - name: empty 15 | run: echo 'Nothing to see here' -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go tests 2 | 3 | on: 4 | push: 5 | branches: [ "develop" ] 6 | pull_request: 7 | branches: [ "develop" ] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: '1.21' 22 | cache-dependency-path: go.sum 23 | - name: Test 24 | run: go test ./... 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editors 2 | *.swp 3 | *~ 4 | 5 | # build 6 | /base 7 | /.vscode 8 | /.venv 9 | /vendor 10 | /test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zigbee Home 2 | 3 | Project that aims to provide similar functionality to [ESPHome](https://github.com/esphome/esphome), but for Zigbee devices. 4 | 5 | # :information_source: Note 6 | `dev` branch is for experiments and exploration. 7 | It cannot be used to determine quality of resulting project. 8 | 9 | ## Status 10 | 11 | Currently work is being carried to develop CLI application and adding sensors. 12 | 13 | Priorities can be ordered as: 14 | * Board(bootloader) support 15 | * Adding known sensors 16 | * Adding Zigbee clusters & templates for unavailable clusters in ZBOSS 17 | 18 | ## Examples 19 | Some examples of configuration files can be seen in `examples` directory. They do not provide complete and full configuration option usage, at least for now. 20 | 21 | ### Licenses 22 | This project uses information from ZBOSS SDK, license for which can be found in `zboss_license.txt`. 23 | 24 | ### References 25 | * nRF Connect SDK 26 | * * [Download page](https://www.nordicsemi.com/Products/Development-software/nRF-Connect-SDK) 27 | * * [Source](https://github.com/nrfconnect/sdk-nrf) 28 | * * [Documentation](http://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest) 29 | * [Zephyr project](https://www.zephyrproject.org/) 30 | * [ESPHome](https://esphome.io/) 31 | * [Zigbee Cluster Library](https://csa-iot.org/wp-content/uploads/2022/01/07-5123-08-Zigbee-Cluster-Library-1.pdf) 32 | 33 | ## Special thanks 34 | * @rsporsche - for donating nRF52840 DK board 35 | * @Hedda - for informational support 36 | -------------------------------------------------------------------------------- /cmd/zigbee/firmware/flash.go: -------------------------------------------------------------------------------- 1 | package firmware 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func flashCmd() *cli.Command { 10 | return &cli.Command{ 11 | Name: "flash", 12 | Usage: "flash the firmware", 13 | Action: func(ctx *cli.Context) error { 14 | configFileName := getConfigFile(ctx) 15 | 16 | cfg, err := parseConfig(configFileName) 17 | if err != nil { 18 | return fmt.Errorf("prepare config: %w", err) 19 | } 20 | 21 | // Will work in the future. 22 | workDir := ctx.String("workdir") 23 | if workDir == "" { 24 | workDir = "." 25 | } 26 | 27 | // generator := generate.NewGenerator(cfg) 28 | 29 | // if err := generator.Generate(workDir); err != nil { 30 | // return fmt.Errorf("generate base: %w", err) 31 | // } 32 | 33 | flasher := NewFlasher(cfg) 34 | 35 | return flasher.Flash(ctx.Context, cfg, workDir) 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cmd/zigbee/firmware/flasher.go: -------------------------------------------------------------------------------- 1 | package firmware 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/ffenix113/zigbee_home/cmd/zigbee/firmware/flasher" 9 | "github.com/ffenix113/zigbee_home/config" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var knownFlashers = map[string]any{ 14 | "adafruit": flasher.NewAdafruitNRFUtil(), 15 | "west": &flasher.West{}, 16 | "nrfutil": flasher.NewNRFUtil(), 17 | "mcuboot": &flasher.MCUBoot{}, 18 | } 19 | 20 | type Flasher interface { 21 | Flash(ctx context.Context, device *config.Device, workDir string) error 22 | } 23 | 24 | func NewFlasher(device *config.Device) Flasher { 25 | // Sane default to flash with `west`. 26 | flasherName := "west" 27 | if device.General.Flasher != "" { 28 | flasherName = device.General.Flasher 29 | } 30 | 31 | flasher, ok := knownFlashers[flasherName] 32 | if !ok { 33 | panic(fmt.Sprintf("flasher %q is not defined", flasherName)) 34 | } 35 | 36 | if len(device.General.FlasherOptions) != 0 { 37 | if err := readFlasherConfig(device.General.FlasherOptions, flasher); err != nil { 38 | panic(fmt.Sprintf("read flasher config: %s", err.Error())) 39 | } 40 | } 41 | 42 | return flasher.(Flasher) 43 | } 44 | 45 | func readFlasherConfig(opts map[string]any, flasher any) error { 46 | bts, err := yaml.Marshal(opts) 47 | if err != nil { 48 | return fmt.Errorf("marshal flasher config: %w", err) 49 | } 50 | 51 | dec := yaml.NewDecoder(bytes.NewReader(bts)) 52 | dec.KnownFields(true) 53 | 54 | if err := dec.Decode(flasher); err != nil { 55 | return fmt.Errorf("decode flasher options: %w", err) 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /cmd/zigbee/firmware/flasher/adafruit_nrfutil.go: -------------------------------------------------------------------------------- 1 | package flasher 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ffenix113/zigbee_home/config" 8 | "github.com/ffenix113/zigbee_home/runner" 9 | ) 10 | 11 | // AdafruitNRFUtil will flash the board with `nrfutil` application. 12 | // For now it only works if `adafruit-nrfutil` application is in current PATH. 13 | // 14 | // So, by default, if it is installed into venv - it will not work. 15 | // 16 | // https://docs.zephyrproject.org/latest/boards/arm/nrf52840dongle_nrf52840/doc/index.html#option-1-using-the-built-in-bootloader-only 17 | type AdafruitNRFUtil struct { 18 | Port string 19 | } 20 | 21 | func NewAdafruitNRFUtil() *AdafruitNRFUtil { 22 | return &AdafruitNRFUtil{ 23 | Port: "/dev/ttyACM0", 24 | } 25 | } 26 | 27 | func (n AdafruitNRFUtil) Flash(ctx context.Context, device *config.Device, workDir string) error { 28 | // 1. Package the app 29 | packager := runner.NewCmd( 30 | "adafruit-nrfutil", 31 | "dfu", 32 | "genpkg", 33 | "--dev-type", 34 | "0x0052", 35 | "--application", 36 | "build/zephyr/zephyr.hex", 37 | "dfu.zip", 38 | ) 39 | if err := packager.Run(ctx, runner.WithWorkDir(workDir)); err != nil { 40 | return fmt.Errorf("create dfu package: %w", err) 41 | } 42 | 43 | // 2. Flash the app 44 | flasher := runner.NewCmd( 45 | "adafruit-nrfutil", 46 | "dfu", 47 | "serial", 48 | "--package", 49 | "dfu.zip", 50 | "-p", 51 | n.Port, 52 | "--singlebank", 53 | "-b", 54 | "115200", 55 | ) 56 | if err := flasher.Run(ctx, runner.WithWorkDir(workDir)); err != nil { 57 | return fmt.Errorf("flash the board: %w", err) 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /cmd/zigbee/firmware/flasher/mcuboot.go: -------------------------------------------------------------------------------- 1 | package flasher 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ffenix113/zigbee_home/config" 8 | "github.com/ffenix113/zigbee_home/runner" 9 | ) 10 | 11 | // MCUBoot will flash the board that has mcuboot. 12 | // https://docs.zephyrproject.org/latest/boards/arm/nrf52840dongle_nrf52840/doc/index.html#option-2-using-mcuboot-in-serial-recovery-mode 13 | type MCUBoot struct{} 14 | 15 | func (MCUBoot) Flash(ctx context.Context, device *config.Device, workDir string) error { 16 | toolchainsPath := device.General.GetToochainsPath() 17 | opts := []runner.CmdOpt{ 18 | runner.WithWorkDir(workDir), 19 | runner.WithToolchainPath(toolchainsPath.NCS, toolchainsPath.Zephyr), 20 | } 21 | 22 | // 1. Sign the firmware 23 | sign := runner.NewCmd( 24 | "west", 25 | "sign", 26 | "-t", 27 | "imgtool", 28 | "--bin", 29 | "--no-hex", 30 | "-d", 31 | "build/zephyr", 32 | "-B", 33 | "zephyr.signed.bin", 34 | "--", 35 | "--key", 36 | "some_key.pem", 37 | ) 38 | if err := sign.Run(ctx, opts...); err != nil { 39 | return fmt.Errorf("sign app: %w", err) 40 | } 41 | 42 | // 2. Flash the image 43 | flash := runner.NewCmd( 44 | "mcumgr", 45 | "--conntype", 46 | "serial", 47 | "--connstring", 48 | "dev=/dev/ttyACM0,baud=115200", 49 | "image", 50 | "upload", 51 | "-e", 52 | "zephyr.signed.bin", 53 | ) 54 | if err := flash.Run(ctx, opts...); err != nil { 55 | return fmt.Errorf("flash the board: %w", err) 56 | } 57 | 58 | // 3. Reset the board 59 | reset := runner.NewCmd( 60 | "mcumgr", 61 | "--conntype", 62 | "serial", 63 | "--connstring", 64 | "dev=/dev/ttyACM0,baud=115200", 65 | "reset", 66 | ) 67 | if err := reset.Run(ctx, opts...); err != nil { 68 | return fmt.Errorf("reset the board: %w", err) 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /cmd/zigbee/firmware/flasher/nrfutil.go: -------------------------------------------------------------------------------- 1 | package flasher 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ffenix113/zigbee_home/config" 8 | "github.com/ffenix113/zigbee_home/runner" 9 | ) 10 | 11 | // NRFUtil will flash the board with `nrfutil` application. 12 | // https://docs.zephyrproject.org/latest/boards/arm/nrf52840dongle_nrf52840/doc/index.html#option-1-using-the-built-in-bootloader-only 13 | type NRFUtil struct { 14 | Port string 15 | } 16 | 17 | func NewNRFUtil() *NRFUtil { 18 | return &NRFUtil{ 19 | Port: "/dev/ttyACM0", 20 | } 21 | } 22 | 23 | func (n NRFUtil) Flash(ctx context.Context, device *config.Device, workDir string) error { 24 | // 1. Package the app 25 | packager := runner.NewCmd( 26 | "nrfutil", 27 | "pkg", 28 | "generate", 29 | "--hw-version", 30 | "52", 31 | "--sd-req", 32 | "0x00", 33 | "--application", 34 | "build/zephyr/zephyr.hex", 35 | "--application-version", 36 | "1", 37 | "dfu.zip", 38 | ) 39 | if err := packager.Run(ctx, runner.WithWorkDir(workDir)); err != nil { 40 | return fmt.Errorf("create dfu package: %w", err) 41 | } 42 | 43 | // 2. Flash the app 44 | flasher := runner.NewCmd( 45 | "nrfutil", 46 | "dfu", 47 | "usb-serial", 48 | "-pkg", 49 | "dfu.zip", 50 | "-p", 51 | n.Port, 52 | ) 53 | if err := flasher.Run(ctx, runner.WithWorkDir(workDir)); err != nil { 54 | return fmt.Errorf("flash the board: %w", err) 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /cmd/zigbee/firmware/flasher/west.go: -------------------------------------------------------------------------------- 1 | package flasher 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ffenix113/zigbee_home/config" 8 | "github.com/ffenix113/zigbee_home/runner" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | type West struct { 13 | opts map[string]any 14 | } 15 | 16 | func (w West) Flash(ctx context.Context, device *config.Device, workDir string) error { 17 | opts := make([]string, 0, len(w.opts)*2+1) 18 | opts = append(opts, "flash") 19 | 20 | for opt, val := range w.opts { 21 | opts = append(opts, "--"+opt, fmt.Sprint(val)) 22 | } 23 | 24 | toolchainsPath := device.General.GetToochainsPath() 25 | return runner.NewCmd("west", opts...).Run( 26 | ctx, 27 | runner.WithWorkDir(workDir), 28 | runner.WithToolchainPath(toolchainsPath.NCS, toolchainsPath.Zephyr), 29 | ) 30 | } 31 | 32 | func (w *West) UnmarshalYAML(node *yaml.Node) error { 33 | return node.Decode(&w.opts) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/zigbee/firmware/root.go: -------------------------------------------------------------------------------- 1 | package firmware 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | func RootCmd() *cli.Command { 8 | return &cli.Command{ 9 | Name: "firmware", 10 | Usage: "firmware operations like build & flash", 11 | Subcommands: []*cli.Command{ 12 | buildCmd(), 13 | flashCmd(), 14 | }, 15 | Flags: []cli.Flag{ 16 | &cli.StringFlag{ 17 | Name: "workdir", 18 | Usage: "change the working directory for the build process (currently does not do anything)", 19 | }, 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cmd/zigbee/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "os" 8 | "runtime/debug" 9 | "slices" 10 | 11 | "github.com/ffenix113/zigbee_home/cmd/zigbee/firmware" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | func main() { 16 | log.SetFlags(log.Lmsgprefix | log.LstdFlags | log.Lshortfile) 17 | 18 | app := &cli.App{ 19 | Name: "zigbee", 20 | Usage: "Zigbee Home CLI application", 21 | Commands: []*cli.Command{ 22 | firmware.RootCmd(), 23 | }, 24 | Flags: []cli.Flag{ 25 | &cli.StringFlag{ 26 | Name: "config", 27 | Value: "zigbee.yaml", 28 | }, 29 | &cli.BoolFlag{ 30 | Name: "version", 31 | Aliases: []string{"v"}, 32 | }, 33 | }, 34 | Action: func(ctx *cli.Context) error { 35 | if ctx.Bool("version") { 36 | return printVersion() 37 | } 38 | 39 | cli.ShowAppHelpAndExit(ctx, 0) 40 | 41 | return nil 42 | }, 43 | } 44 | 45 | if err := app.RunContext(context.Background(), os.Args); err != nil { 46 | log.Println(err.Error()) 47 | os.Exit(1) 48 | } 49 | } 50 | 51 | func printVersion() error { 52 | buildInfo, ok := debug.ReadBuildInfo() 53 | if !ok { 54 | return errors.New("could not read build information") 55 | } 56 | 57 | vcsProps := make([]debug.BuildSetting, 0, 2) 58 | 59 | for _, setting := range buildInfo.Settings { 60 | if !slices.Contains([]string{"vcs.revision", "vcs.modified"}, setting.Key) { 61 | continue 62 | } 63 | 64 | vcsProps = append(vcsProps, setting) 65 | if len(vcsProps) == cap(vcsProps) { 66 | break 67 | } 68 | } 69 | 70 | slices.SortFunc(vcsProps, func(a, b debug.BuildSetting) int { 71 | if a.Key > b.Key { 72 | return 1 73 | } 74 | 75 | return -1 76 | }) 77 | 78 | // Information that will help with investigation of 79 | log.Printf("%s, tag:%s, version:%s", buildInfo.GoVersion, buildInfo.Main.Version, vcsProps) 80 | 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /config/device_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "os" 7 | "path" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestConfigLoad(t *testing.T) { 14 | t.Run("valid small", func(t *testing.T) { 15 | data := []byte(`general: {board: some_board}`) 16 | 17 | cfg, err := ParseFromReader(&Device{}, bytes.NewReader(data)) 18 | require.NoError(t, err) 19 | 20 | require.NotNil(t, cfg) 21 | require.Equal(t, "some_board", cfg.General.Board) 22 | }) 23 | 24 | t.Run("invalid small", func(t *testing.T) { 25 | data := []byte(`some: {unknown: some_board}`) 26 | 27 | cfg, err := ParseFromReader(&Device{}, bytes.NewReader(data)) 28 | require.Error(t, err) 29 | require.Nil(t, cfg) 30 | }) 31 | } 32 | 33 | // TestLoadExamples will try to load example configurations and check if they are valid. 34 | // It will not compile/generate them, only check if configuration is correct. 35 | // 36 | // In the future this should be replaced with actual building of the examples. 37 | func TestLoadExamples(t *testing.T) { 38 | cwd, err := os.Getwd() 39 | require.NoError(t, err) 40 | 41 | examplesPath := path.Join(cwd, "..", "examples") 42 | examples, err := os.ReadDir(examplesPath) 43 | if err != nil { 44 | t.Skipf("cannot read examples dir: %s", err.Error()) 45 | } 46 | 47 | if len(examples) == 0 { 48 | t.Skip("no example directories") 49 | } 50 | 51 | for _, example := range examples { 52 | if !example.IsDir() { 53 | continue 54 | } 55 | 56 | t.Run(example.Name(), func(t *testing.T) { 57 | examplePath := path.Join(examplesPath, example.Name()) 58 | // We need to change directory as some files can contain includes, 59 | // which will be relative to the configuration file. 60 | require.NoError(t, os.Chdir(examplePath)) 61 | 62 | defaultConfig, err := os.Open(path.Join(examplePath, "zigbee.yaml")) 63 | if errors.Is(err, os.ErrNotExist) { 64 | t.Skip("default config not found") 65 | } 66 | 67 | require.NoError(t, err) 68 | 69 | defer defaultConfig.Close() 70 | 71 | _, err = ParseFromReader(&Device{}, defaultConfig) 72 | require.NoError(t, err) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/tag_resolver.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | // tagsResolver does required actions to resolve tags 13 | // inside configuration file. 14 | // For example does inclusion of external files. 15 | type tagsResolver struct { 16 | // maxIncludeDepth tells how deep includes can be 17 | // when file includes file, includes file, ... 18 | maxIncludeDepth int 19 | } 20 | 21 | func newTagsResolver() *tagsResolver { 22 | const defaultMaxIncludeDepth = 3 23 | 24 | return &tagsResolver{ 25 | maxIncludeDepth: defaultMaxIncludeDepth, 26 | } 27 | } 28 | 29 | func (r *tagsResolver) resolve(node *yaml.Node, depth int) error { 30 | var newNode *yaml.Node 31 | 32 | var err error 33 | 34 | switch node.Tag { 35 | case "!include": 36 | newNode, err = r.getIncludedNode(node.Value) 37 | if err != nil { 38 | return fmt.Errorf("include: %w", err) 39 | } 40 | 41 | if depth+1 > r.maxIncludeDepth { 42 | log.Fatalf("max include resolution depth of %d reached", depth) 43 | } 44 | 45 | if err := r.resolve(newNode, depth+1); err != nil { 46 | return fmt.Errorf("included file: %w", err) 47 | } 48 | case "!env": 49 | newNode, err = r.getEnvNode(node.Value) 50 | if err != nil { 51 | return fmt.Errorf("env: %w", err) 52 | } 53 | } 54 | 55 | if newNode != nil { 56 | *node = *newNode 57 | } 58 | 59 | for _, content := range node.Content { 60 | if err := r.resolve(content, depth); err != nil { 61 | return fmt.Errorf("content: %w", err) 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (r *tagsResolver) getIncludedNode(includePath string) (*yaml.Node, error) { 69 | includePath, err := filepath.Abs(includePath) 70 | if err != nil { 71 | return nil, fmt.Errorf("get absolute include path of %q: %w", includePath, err) 72 | } 73 | 74 | fileBts, err := os.ReadFile(includePath) 75 | if err != nil { 76 | return nil, fmt.Errorf("read file %q: %w", includePath, err) 77 | } 78 | 79 | var n yaml.Node 80 | if err := yaml.Unmarshal(fileBts, &n); err != nil { 81 | return nil, fmt.Errorf("unmarshal %q: %w", includePath, err) 82 | } 83 | 84 | return r.getUnmarshaledNode(&n), nil 85 | } 86 | 87 | func (r *tagsResolver) getEnvNode(env string) (*yaml.Node, error) { 88 | envValue, ok := os.LookupEnv(env) 89 | if !ok || envValue == "" { 90 | return nil, nil 91 | } 92 | 93 | var n yaml.Node 94 | if err := yaml.Unmarshal([]byte(envValue), &n); err != nil { 95 | return nil, fmt.Errorf("unmarshal %q: %w", envValue, err) 96 | } 97 | 98 | return r.getUnmarshaledNode(&n), nil 99 | } 100 | 101 | func (r *tagsResolver) getUnmarshaledNode(n *yaml.Node) *yaml.Node { 102 | if n.Kind == yaml.DocumentNode { 103 | return n.Content[0] 104 | } 105 | 106 | return n 107 | } 108 | -------------------------------------------------------------------------------- /docs/features/factory_reset.md: -------------------------------------------------------------------------------- 1 | In some situations it is necessary to have a way to disconnect device from the network it was connected to. 2 | 3 | For example: 4 | * Device will be used in different network. 5 | * Previous network is not available (i.e. on configuration change). 6 | * Device was not correctly disconnected from network, resulting in device having network configuration, but network does not accept the device. 7 | 8 | To resolve this issues firmware provides a way to set a button that will act as a factory reset button: 9 | ```yml 10 | board: 11 | factory_reset_button: btn1 12 | buttons: 13 | - id: btn1 14 | ``` 15 | 16 | To perform a factory reset user needs to hold `btn1` for more than 5 seconds and then release. 17 | After this the device will remove current network configuration and automatically try to pair with any open Zigbee network. -------------------------------------------------------------------------------- /docs/img/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffenix113/zigbee_home/17bb7b9e9d375e756da9e38913f53303937fb66a/docs/img/build.png -------------------------------------------------------------------------------- /docs/img/create_new_build_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffenix113/zigbee_home/17bb7b9e9d375e756da9e38913f53303937fb66a/docs/img/create_new_build_config.png -------------------------------------------------------------------------------- /docs/img/new_build_config_params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffenix113/zigbee_home/17bb7b9e9d375e756da9e38913f53303937fb66a/docs/img/new_build_config_params.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Zigbee_home 2 | 3 | !!! note 4 | The application is still in early stage and active development, and while it already works and can produce source that will be successfully compiled to a firmware - there are still some missing pieces, features, options and possible **breaking changes**. 5 | 6 | Please see navigation menu to the left to start. 7 | -------------------------------------------------------------------------------- /docs/install_zigbee_home_cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Install zigbee_home CLI 3 | --- 4 | 5 | CLI is the main entry point for generating, building and flashing the firmware from one single place. 6 | 7 | Currently installing from source is available. In later stages compiled binaries will be provided on Github. 8 | 9 | [Go installation](https://go.dev/doc/install) is required to build CLI from source. Go version of `1.21.4` or later is required. 10 | One might be already provided in your Linux distribution or `brew` on Mac. 11 | 12 | ## Installing using `go install` 13 | This will be best for users that don't want to mess with source code or have no need to modify internal functionality. 14 | 15 | To install the CLI just run 16 | ```bash 17 | go install github.com/ffenix113/zigbee_home/cmd/zigbee@develop 18 | ``` 19 | This will install the CLI and it can be later used by running `zigbee` from command line. 20 | 21 | !!! note 22 | The name of the executable is quite generic, if this is a problem - please see next runnning method. 23 | 24 | ## Running from source without installation 25 | This solution is for more advanced users, that have some knowledge in git and Go. 26 | 27 | To run an executable first you would need to pull the source code of the project to some directory: 28 | ```bash 29 | $ git clone git@github.com:ffenix113/zigbee_home.git 30 | ``` 31 | Inside the cloned directory run 32 | ```bash 33 | $ go run ./zigbee_home/cmd/zigbee/... [args...] 34 | ``` 35 | 36 | This will not add any executables in your PATH. Instead you would need to execute `go run` command mentioned above each time to run the CLI. 37 | 38 | ### Updating the source code 39 | When using this method the code will not be updated automatically in any way. To do this you would need to navigate to cloned repository and run 40 | ```bash 41 | $ git pull 42 | ``` -------------------------------------------------------------------------------- /docs/sensors/supported_sensors.md: -------------------------------------------------------------------------------- 1 | ## Supporting more sensors 2 | Depending on a sensor it might be either easy, or rather involving to provide support for a sensor. 3 | 4 | Currently there is a work to change how sensors are polled to make all sensors to work through Zephyr's Sensor API, which would result in same codebase for all sensors in the main app, while drivers will implement interface required to talk to the sensors. 5 | 6 | When this change will be done - all sensors officially supported by Zephyr, will be supported. Note that the sensor will be supported only in the context of [Zigbee Cluster Library](https://zigbeealliance.org/wp-content/uploads/2021/10/07-5123-08-Zigbee-Cluster-Library.pdf), meaning that if sensor supports some measurements that Zigbee Cluster Library does not support - it will not be available in Zigbee. 7 | 8 | ## Supported sensors 9 | !!! note 10 | Full per-sensor options will be defined later. For now please refer to [configuration file](../using_the_cli/configuration_file.md) documentation or [latest configuration](https://github.com/ffenix113/zigbee_home/blob/develop/zigbee.yml) in repository for sensor configuration options. 11 | 12 | * `bme280` & `bme680` - Bosch BME280 / BME680 13 | * `dht` - Aosong DHT11 / DHT22 / AM2302 14 | * `scd4x` - Sensirion SCD41 ([driver](https://github.com/nobodyguy/sensirion_zephyr_drivers) by [nobodyguy](https://github.com/nobodyguy)) 15 | -------------------------------------------------------------------------------- /docs/supported_boards.md: -------------------------------------------------------------------------------- 1 | ## Currently supported boards 2 | This list can be better thought of as "supported bootloaders" instead of the boards. 3 | 4 | * nRF52840 Dongle (`nrf52840dongle_nrf52840`) - Uses `nrf52_legacy` bootloader. 5 | * Arduino Nano 33 BLE (Sense) (`arduino_nano_33_ble`) - Uses `arduino` bootloader. With this configuration other boards with nrf52840 and Bossac bootloader(== Arduino bootloader) might be supported. Give it a try! 6 | 7 | ### Experimental 8 | * Any Adafruit bootloader-based boards. 9 | : This includes support for bootloaders with SD S132, S140 v6 and v7. A `bootloader` configuration option must be set to a necessary bootloader. Supported bootloader versions are 10 | * `adafruit_nrf52_sd132` 11 | * `adafruit_nrf52_sd140_v6` 12 | * `adafruit_nrf52_sd140_v7` -------------------------------------------------------------------------------- /docs/using_the_cli/building_the_firmware.md: -------------------------------------------------------------------------------- 1 | # Building the firmware 2 | 3 | !!! note 4 | The project is still in development, and the steps to build and flash the firmware will change in the future. 5 | 6 | ## Building 7 | 8 | For now zigbee_home CLI cannot build the application directly, unless run in a configured nRF Connect & Zephyr environment. 9 | 10 | As such while this is the case - we will rely on the VS Code with nRF Connect extension to build the firmware. 11 | 12 | ### Source Generation 13 | CLI generates source, which can then be built and flashed. 14 | 15 | "Source" includes C source code, app config(`proj.conf`) and overlay (`app.overlay`). 16 | 17 | 18 | ### Preparing environment 19 | Nordic provides an installation [tutorial video](https://www.youtube.com/watch?v=EAJdOqsL9m8&t=0s) for setting up the environment for VS Code. 20 | 21 | This preparation is only needed once, and after all the tools are set up - you don't need to do it again. 22 | 23 | ### Build with zigbee_home CLI 24 | Assuming configuration file is located at `./zigbee_test.yml` and directory for generated & built firmware should be `./firmware` the command to generate & build the firmware will be: 25 | ```sh 26 | go run ./cmd/zigbee --config ./zigbee_test.yml firmware --workdir ./firmware build 27 | ``` 28 | 29 | Only source code can be generated by adding `--only-generate` flag after `build`: 30 | ```sh 31 | go run ./cmd/zigbee --config ./zigbee_test.yml firmware --workdir ./firmware build --only-generate 32 | ``` 33 | 34 | Note: `build` command will not clear the work directory, so if it already contains some additional files they may be used while building the firmware. 35 | 36 | This also applies when removing features from configuration and re-building the firmware: some files might be left after feature that requires it was removed. 37 | It could lead to a build that is failing. 38 | 39 | ### Build from extension 40 | 41 | To build the firmware just open the generated firmware source file directory in VS Code. 42 | Then press the nRF Connect extension in the sidebard and choose `Create new build configuration`: 43 | 44 | ![New build configuration](../img/create_new_build_config.png) 45 | 46 | This will open a new tab with some options. The only thing that needs to be changed is to provide proper board name. 47 | 48 | ![Adjust the board name](../img/new_build_config_params.png) 49 | 50 | After this you can press "Build configuration" and wait for the build to finish. 51 | 52 | !!! note 53 | While building there may be some warnings, but not errors. If there is an error while building the source it is most probably that there is some mis-configuration in `zigbee.yml`, or other provided configuration while generating source from CLI. 54 | 55 | #### Building firmware again 56 | After first build it is not necessary to create new build configuration again. 57 | 58 | Instead open the nRF Connect extension, choose already existing configuration by clicking on it and click `Build` in `Actions` tab: 59 | 60 | ![Build existing configuration](../img/build.png) 61 | -------------------------------------------------------------------------------- /docs/using_the_cli/flash_firmware.md: -------------------------------------------------------------------------------- 1 | ### Flashing 2 | CLI can flash already built application with a couple of methods: 3 | - nrfutil 4 | - mcuboot 5 | - west 6 | - adafruit (if available in current PATH) 7 | 8 | Device already has to be in a mode that will allow flashing(DFU, for example). 9 | 10 | Example: 11 | ```sh 12 | go run ./cmd/zigbee --config ./zigbee_test.yml firmware --workdir ./firmware flash 13 | ``` 14 | 15 | Also see [full example](index.md#full-example) for flashing instructions. -------------------------------------------------------------------------------- /docs/using_the_cli/generating_source_code.md: -------------------------------------------------------------------------------- 1 | TBD. Please see [Using the CLI](index.md#generate-source-code) and [full example](index.md#full-example) for generation instructions. -------------------------------------------------------------------------------- /docs/using_the_cli/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using the CLI 3 | --- 4 | 5 | Zigbee_home CLI provides a way to do necessary actions to produce a device that is flashed with firmware generated & built with this project. 6 | 7 | There are 3 things that CLI is responsible for: 8 | * Generating the source code 9 | * Building the firmware 10 | * Flashing built firmware 11 | 12 | Other functionalities might be available in the future. 13 | 14 | ## CLI commands 15 | 16 | #### Global flags 17 | 18 | At top level there is only option to provide path for configuration file, if it is not located in working directory. 19 | ``` 20 | --config 21 | ``` 22 | 23 | Default configuration file called `zigbee.yml`. 24 | 25 | For example 26 | 27 | ```bash 28 | $ go run ./cmd/zigbee/... --config ../zigbee.yml 29 | $ # Or if used with installed CLI 30 | $ zigbee --config ../zigbee.yml 31 | ``` 32 | 33 | ### `firmware` 34 | This is a base command for sub-commands, and so it is not useful on it's own. 35 | ```bash 36 | zigbee firmware 37 | ``` 38 | 39 | #### Flags 40 | `--workdir` 41 | 42 | : Provide an optional working directory. This is where source code will be generated and used from. 43 | 44 | Defaults to current working directory. 45 | 46 | 47 | #### Generate source code 48 | Currently, generation of source code is not a separate action, as it is a part of building the firmware. A project goal is to do single call to do all the necessary actions, where user provides a configuration file and after running the CLI - a device will be flashed with generated firmware. 49 | 50 | To generate source code from configuration file user must call 51 | ``` 52 | firmware build --only-generate 53 | ``` 54 | 55 | For example 56 | ```bash 57 | $ zigbee --config ../zigbee.yml firmware build --only-generate 58 | ``` 59 | 60 | #### Build the firmware 61 | !!! warning 62 | Currently each time the build is requested - CLI will re-generate the source code. This means that any manual changes to the generated source code would be removed. 63 | 64 | !!! note 65 | As building firmware requires having environment set correctly it is not possible to simply run this command from normal shell environment. 66 | If necessary this command can be executed from terminal that nRF Connect VS Code extension can create by going to `nRF Connect` extension -> in `Applications` right click necessary build configuration and select `Start new terminal here`. 67 | This will open a new terminal window with correct environment, in which this command can be executed. 68 | 69 | To generate and build the firmware use 70 | ``` 71 | firmware build 72 | ``` 73 | 74 | This command does not need to be executed inside the generate source directory. Instead `--workdir` flag can be provided to point to necessary directory with source code. 75 | 76 | For example 77 | ```bash 78 | $ go run ./cmd/zigbee/... firmware --workdir ~/firmware/soil_moisture_sensor build 79 | ``` 80 | 81 | ### Flashing the firmware 82 | To flash the firmware user needs to set board to bootloader mode and then run 83 | ``` 84 | firmware flash 85 | ``` 86 | For example 87 | ```bash 88 | $ zigbee firmware --workdir ~/firmware/soil_moisture_sensor flash 89 | ``` 90 | 91 | ## Full example 92 | This is a full example of what a typical flow for creating a configuration, generating & building source code and then flashing the device. 93 | 94 | Example makes some assumptions: 95 | 96 | - Board is nRF52840 Dongle with stock bootloader 97 | - Generated source code will be located in `./firmware` 98 | 99 | ```console 100 | $ cat zigbee.yml 101 | general: 102 | runevery: 2m 103 | board: nrf52840dongle_nrf52840 104 | flasher: nrfutil 105 | flasheroptions: 106 | port: /dev/ttyACM1 107 | board: 108 | i2c: 109 | - id: i2c0 110 | port: 0 111 | sda: 29 112 | scl: 31 113 | sensors: 114 | - type: scd4x 115 | i2c: 116 | id: i2c0 117 | $ ls 118 | zigbee.yml 119 | $ zigbee firmware --workdir ./firmware build --only-generate 120 | $ # Now user goes to VS Code, opens the ./firmware directory and compiles the source code 121 | $ # See 'Building the firmware' page for instructions on how to do it. 122 | $ # As a verification that the firmware was built correctly execute following command: 123 | $ ls ./firmware/build/zephyr/zephyr.hex 124 | zephyr.hex 125 | $ # User starts bootloader mode on the board by clicking reset button twice 126 | $ zigbee firmware --workdir ./firmware flash 127 | ``` -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | This folder shows some examples on the ways to configure the device, bootloader and sensors. 4 | 5 | Most of the examples can be combined, if they are not overwriting each-others configuration, or if user merges them with most appropriate options. 6 | 7 | The examples may not provide configurations that will definitely run on your hardware configuration, instead providing examples of options that can be used to achieve something. 8 | 9 | ## Provided examples 10 | * `basic` - the most featureless firmware that can be build with this project. 11 | * `adafruit_bootloader` - shows how to specify different supported bootloader(in this case Adafruit) for the board. 12 | * `sensor_bme280` - usage of Bosch BME280 sensoron I2C interface. 13 | * `config_tags` - configuration tags that can help define more extensible/generic configuration. 14 | * `debug` - simple debug configuration that will show power & connection state + USB logging. 15 | * `contact` - configuration that uses IAS Zone to add a contact sensor. -------------------------------------------------------------------------------- /examples/adafruit_bootloader/README.md: -------------------------------------------------------------------------------- 1 | ## Adafruit bootloader 2 | 3 | Some boards come with Adafruit bootloader, and many more can be flashed to use it. 4 | 5 | This example provides configuration that can be used to build a firmware that can be flashed and run on such boards. -------------------------------------------------------------------------------- /examples/adafruit_bootloader/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: arduino_nano_33_ble 3 | 4 | board: 5 | # Other valid Adafruit bootloaders include 6 | # adafruit_nrf52_sd130 7 | # adafruit_nrf52_sd140_v7 8 | bootloader: adafruit_nrf52_sd140_v6 -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | ## Basic configuration with no sensors 2 | 3 | This example shows bare minimum configuration that will build the firmware. 4 | 5 | It does not provide additional features. It can only be paired to coordinator and describe itself. -------------------------------------------------------------------------------- /examples/basic/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: nrf52840dongle_nrf52840 3 | -------------------------------------------------------------------------------- /examples/battery_measurement/README.md: -------------------------------------------------------------------------------- 1 | ## Battery measurement 2 | 3 | Example shows how to measure voltage on analog pin and report it as remaining percentage of battery. 4 | 5 | Note: this example shows simple configuration, while ADC definition can be adjasted more granularly. 6 | More configuration options will be explained in documentation, and/or later added to this example -------------------------------------------------------------------------------- /examples/battery_measurement/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: arduino_nano_33_ble 3 | 4 | sensors: 5 | - type: power_config 6 | adc_pin: 0.04 7 | # Voltages are defined in mV. 8 | battery_rated_voltage: 3300 9 | battery_voltage_min_threshold: 2500 -------------------------------------------------------------------------------- /examples/config_tags/README.md: -------------------------------------------------------------------------------- 1 | ## Tags that can be used in configuration file to make it more dynamic/configurable. 2 | 3 | 4 | Tags included in this example: 5 | * `!include` - tag can help split configuration into multiple files, allowing to re-use parts of configuration in multiple files files. 6 | * `!env` - fetch value from environment and replace the tag with it. If env variable is not present - configuration may become invalid and might break the build process. 7 | 8 | 9 | Env variable `ZBHOME_BOARD_NAME` will be used to set the board name, and file `board.yaml` contains board definition that will be included inside the main config file. -------------------------------------------------------------------------------- /examples/config_tags/board.yaml: -------------------------------------------------------------------------------- 1 | bootloader: adafruit_nrf52_sd140_v6 2 | is_router: true -------------------------------------------------------------------------------- /examples/config_tags/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: !env ZBHOME_BOARD_NAME 3 | 4 | board: !include board.yaml -------------------------------------------------------------------------------- /examples/contact/README.md: -------------------------------------------------------------------------------- 1 | ## IAS Zone as contact sensor 2 | 3 | This example provides definition for contact sensor. 4 | 5 | Each time the state of the button `button0` will change - board will update coordinator to show the new value. 6 | -------------------------------------------------------------------------------- /examples/contact/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: nrf52840dongle_nrf52840 3 | 4 | board: 5 | buttons: 6 | - id: button0 7 | 8 | sensors: 9 | - type: contact 10 | button: button0 -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/Kconfig: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | config BOARD_ENABLE_DCDC 4 | bool "Enable DCDC mode" 5 | select SOC_DCDC_NRF52X 6 | default y 7 | depends on (BOARD_NICE_NANO || BOARD_NICE_NANO_V2) 8 | 9 | config BOARD_ENABLE_DCDC_HV 10 | bool "High voltage DCDC converter" 11 | select SOC_DCDC_NRF52X_HV 12 | default y 13 | depends on (BOARD_NICE_NANO_V2) 14 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/Kconfig.board: -------------------------------------------------------------------------------- 1 | # nice!nano board configuration 2 | 3 | # Copyright (c) 2020 Pete Johanson 4 | # SPDX-License-Identifier: MIT 5 | 6 | config BOARD_NICE_NANO 7 | bool "nice!nano" 8 | depends on SOC_NRF52840_QIAA 9 | 10 | config BOARD_NICE_NANO_V2 11 | bool "nice!nano v2" 12 | depends on SOC_NRF52840_QIAA 13 | 14 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The ZMK Contributors 2 | # SPDX-License-Identifier: MIT 3 | 4 | if BOARD_NICE_NANO || BOARD_NICE_NANO_V2 5 | 6 | config BOARD 7 | default "nice_nano" 8 | 9 | if USB_DEVICE_STACK 10 | 11 | config USB_NRFX 12 | default y 13 | 14 | endif # USB_DEVICE_STACK 15 | 16 | config BT_CTLR 17 | default BT 18 | 19 | endif # BOARD_NICE_NANO || BOARD_NICE_NANO_V2 20 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/arduino_pro_micro_pins.dtsi: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Pete Johanson 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | / { 8 | pro_micro: connector { 9 | compatible = "arduino-pro-micro"; 10 | #gpio-cells = <2>; 11 | gpio-map-mask = <0xffffffff 0xffffffc0>; 12 | gpio-map-pass-thru = <0 0x3f>; 13 | gpio-map 14 | = <0 0 &gpio0 8 0> /* D0 */ 15 | , <1 0 &gpio0 6 0> /* D1 */ 16 | , <2 0 &gpio0 17 0> /* D2 */ 17 | , <3 0 &gpio0 20 0> /* D3 */ 18 | , <4 0 &gpio0 22 0> /* D4/A6 */ 19 | , <5 0 &gpio0 24 0> /* D5 */ 20 | , <6 0 &gpio1 0 0> /* D6/A7 */ 21 | , <7 0 &gpio0 11 0> /* D7 */ 22 | , <8 0 &gpio1 4 0> /* D8/A8 */ 23 | , <9 0 &gpio1 6 0> /* D9/A9 */ 24 | , <10 0 &gpio0 9 0> /* D10/A10 */ 25 | , <16 0 &gpio0 10 0> /* D16 */ 26 | , <14 0 &gpio1 11 0> /* D14 */ 27 | , <15 0 &gpio1 13 0> /* D15 */ 28 | , <18 0 &gpio1 15 0> /* D18/A0 */ 29 | , <19 0 &gpio0 2 0> /* D19/A1 */ 30 | , <20 0 &gpio0 29 0> /* D20/A2 */ 31 | , <21 0 &gpio0 31 0> /* D21/A3 */ 32 | ; 33 | }; 34 | 35 | pro_micro_a: connector_a { 36 | compatible = "arduino-pro-micro"; 37 | #gpio-cells = <2>; 38 | gpio-map-mask = <0xffffffff 0xffffffc0>; 39 | gpio-map-pass-thru = <0 0x3f>; 40 | gpio-map 41 | = <0 0 &gpio1 15 0> /* D18/A0 */ 42 | , <1 0 &gpio0 2 0> /* D19/A1 */ 43 | , <2 0 &gpio0 29 0> /* D20/A2 */ 44 | , <3 0 &gpio0 31 0> /* D21/A3 */ 45 | , <6 0 &gpio0 22 0> /* D4/A6 */ 46 | , <7 0 &gpio1 0 0> /* D6/A7 */ 47 | , <8 0 &gpio1 4 0> /* D8/A8 */ 48 | , <9 0 &gpio1 6 0> /* D9/A9 */ 49 | , <10 0 &gpio0 9 0> /* D10/A10 */ 50 | ; 51 | }; 52 | }; 53 | 54 | pro_micro_d: &pro_micro {}; 55 | pro_micro_i2c: &i2c0 {}; 56 | pro_micro_spi: &spi1 {}; 57 | pro_micro_serial: &uart0 {}; 58 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/board.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | board_runner_args(nrfjprog "--nrf-family=NRF52" "--softreset") 4 | include(${ZEPHYR_BASE}/boards/common/uf2.board.cmake) 5 | include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) 6 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/nice_nano-pinctrl.dtsi: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 The ZMK Contributors 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | &pinctrl { 7 | uart0_default: uart0_default { 8 | group1 { 9 | psels = ; 10 | bias-pull-up; 11 | }; 12 | group2 { 13 | psels = ; 14 | }; 15 | }; 16 | 17 | uart0_sleep: uart0_sleep { 18 | group1 { 19 | psels = , 20 | ; 21 | low-power-enable; 22 | }; 23 | }; 24 | 25 | i2c0_default: i2c0_default { 26 | group1 { 27 | psels = , 28 | ; 29 | }; 30 | }; 31 | 32 | i2c0_sleep: i2c0_sleep { 33 | group1 { 34 | psels = , 35 | ; 36 | low-power-enable; 37 | }; 38 | }; 39 | 40 | spi1_default: spi1_default { 41 | group1 { 42 | psels = , 43 | , 44 | ; 45 | }; 46 | }; 47 | 48 | spi1_sleep: spi1_sleep { 49 | group1 { 50 | psels = , 51 | , 52 | ; 53 | low-power-enable; 54 | }; 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/nice_nano.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | /dts-v1/; 8 | #include "nice_nano.dtsi" 9 | 10 | / { 11 | chosen { 12 | zmk,battery = &vbatt; 13 | }; 14 | 15 | // Node name must match original "EXT_POWER" label to preserve user settings. 16 | EXT_POWER { 17 | compatible = "zmk,ext-power-generic"; 18 | control-gpios = <&gpio0 13 GPIO_ACTIVE_LOW>; 19 | }; 20 | 21 | vbatt: vbatt { 22 | compatible = "zmk,battery-voltage-divider"; 23 | io-channels = <&adc 2>; 24 | output-ohms = <2000000>; 25 | full-ohms = <(2000000 + 806000)>; 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/nice_nano.dtsi: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #include 8 | #include "nice_nano-pinctrl.dtsi" 9 | #include "arduino_pro_micro_pins.dtsi" 10 | 11 | / { 12 | model = "nice!nano"; 13 | compatible = "nice,nano"; 14 | 15 | chosen { 16 | zephyr,code-partition = &code_partition; 17 | zephyr,sram = &sram0; 18 | zephyr,flash = &flash0; 19 | zephyr,ieee802154 = &ieee802154; 20 | }; 21 | 22 | leds { 23 | compatible = "gpio-leds"; 24 | red_led: led_0 { 25 | gpios = <&gpio0 15 GPIO_ACTIVE_HIGH>; 26 | }; 27 | }; 28 | 29 | aliases { 30 | redled = &red_led; 31 | }; 32 | }; 33 | 34 | &ieee802154 { 35 | status = "okay"; 36 | }; 37 | 38 | 39 | &adc { 40 | status = "okay"; 41 | }; 42 | 43 | &gpiote { 44 | status = "okay"; 45 | }; 46 | 47 | &gpio0 { 48 | status = "okay"; 49 | }; 50 | 51 | &gpio1 { 52 | status = "okay"; 53 | }; 54 | 55 | &i2c0 { 56 | compatible = "nordic,nrf-twi"; 57 | pinctrl-0 = <&i2c0_default>; 58 | pinctrl-1 = <&i2c0_sleep>; 59 | pinctrl-names = "default", "sleep"; 60 | }; 61 | 62 | &spi1 { 63 | compatible = "nordic,nrf-spim"; 64 | pinctrl-0 = <&spi1_default>; 65 | pinctrl-1 = <&spi1_sleep>; 66 | pinctrl-names = "default", "sleep"; 67 | }; 68 | 69 | &uart0 { 70 | compatible = "nordic,nrf-uarte"; 71 | current-speed = <115200>; 72 | pinctrl-0 = <&uart0_default>; 73 | pinctrl-1 = <&uart0_sleep>; 74 | pinctrl-names = "default", "sleep"; 75 | }; 76 | 77 | zephyr_udc0: &usbd { 78 | compatible = "nordic,nrf-usbd"; 79 | status = "okay"; 80 | }; 81 | 82 | 83 | &flash0 { 84 | /* 85 | * For more information, see: 86 | * http://docs.zephyrproject.org/latest/devices/dts/flash_partitions.html 87 | */ 88 | partitions { 89 | compatible = "fixed-partitions"; 90 | #address-cells = <1>; 91 | #size-cells = <1>; 92 | 93 | sd_partition: partition@0 { 94 | reg = <0x00000000 0x00026000>; 95 | }; 96 | code_partition: partition@26000 { 97 | reg = <0x00026000 0x000c6000>; 98 | }; 99 | 100 | /* 101 | * The flash starting at 0x000ec000 and ending at 102 | * 0x000f3fff is reserved for use by the application. 103 | */ 104 | 105 | /* 106 | * Storage partition will be used by FCB/LittleFS/NVS 107 | * if enabled. 108 | */ 109 | storage_partition: partition@ec000 { 110 | reg = <0x000ec000 0x00008000>; 111 | }; 112 | 113 | boot_partition: partition@f4000 { 114 | reg = <0x000f4000 0x0000c000>; 115 | }; 116 | }; 117 | }; 118 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/nice_nano.yaml: -------------------------------------------------------------------------------- 1 | identifier: nice_nano 2 | name: nice!nano 3 | type: mcu 4 | arch: arm 5 | toolchain: 6 | - zephyr 7 | - gnuarmemb 8 | - xtools 9 | supported: 10 | - adc 11 | - usb_device 12 | - ble 13 | - ieee802154 14 | - pwm 15 | - watchdog 16 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/nice_nano.zmk.yml: -------------------------------------------------------------------------------- 1 | file_format: "1" 2 | id: nice_nano 3 | name: nice!nano v1 4 | type: board 5 | arch: arm 6 | outputs: 7 | - usb 8 | - ble 9 | url: https://nicekeyboards.com/nice-nano 10 | exposes: [pro_micro] 11 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/nice_nano_defconfig: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | CONFIG_SOC_SERIES_NRF52X=y 4 | CONFIG_SOC_NRF52840_QIAA=y 5 | CONFIG_BOARD_NICE_NANO=y 6 | 7 | # Enable MPU 8 | CONFIG_ARM_MPU=y 9 | 10 | # enable GPIO 11 | CONFIG_GPIO=y 12 | 13 | # Use pinctrl 14 | CONFIG_PINCTRL=y 15 | 16 | CONFIG_USE_DT_CODE_PARTITION=y 17 | CONFIG_BUILD_OUTPUT_UF2=y 18 | 19 | CONFIG_MPU_ALLOW_FLASH_WRITE=y 20 | CONFIG_NVS=y 21 | CONFIG_SETTINGS_NVS=y 22 | CONFIG_FLASH=y 23 | CONFIG_FLASH_PAGE_LAYOUT=y 24 | CONFIG_FLASH_MAP=y 25 | 26 | CONFIG_ZMK_USB=y 27 | CONFIG_ZMK_BLE=y -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/nice_nano_v2.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | /dts-v1/; 8 | #include "nice_nano.dtsi" 9 | 10 | / { 11 | chosen { 12 | zmk,battery = &vbatt; 13 | }; 14 | 15 | // Node name must match original "EXT_POWER" label to preserve user settings. 16 | EXT_POWER { 17 | compatible = "zmk,ext-power-generic"; 18 | control-gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>; 19 | init-delay-ms = <50>; 20 | }; 21 | 22 | vbatt: vbatt { 23 | compatible = "zmk,battery-nrf-vddh"; 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/nice_nano_v2.yaml: -------------------------------------------------------------------------------- 1 | identifier: nice_nano_v2 2 | name: nice!nano v2 3 | type: mcu 4 | arch: arm 5 | toolchain: 6 | - zephyr 7 | - gnuarmemb 8 | - xtools 9 | supported: 10 | - adc 11 | - usb_device 12 | - ble 13 | - ieee802154 14 | - pwm 15 | - watchdog 16 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/nice_nano_v2.zmk.yml: -------------------------------------------------------------------------------- 1 | file_format: "1" 2 | id: nice_nano_v2 3 | name: nice!nano v2 4 | type: board 5 | arch: arm 6 | outputs: 7 | - usb 8 | - ble 9 | url: https://nicekeyboards.com/nice-nano 10 | exposes: [pro_micro] 11 | -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/nice_nano_v2_defconfig: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | CONFIG_SOC_SERIES_NRF52X=y 4 | CONFIG_SOC_NRF52840_QIAA=y 5 | CONFIG_BOARD_NICE_NANO_V2=y 6 | 7 | # Enable MPU 8 | CONFIG_ARM_MPU=y 9 | 10 | # Use pinctrl 11 | CONFIG_PINCTRL=y 12 | 13 | # enable GPIO 14 | CONFIG_GPIO=y 15 | 16 | CONFIG_USE_DT_CODE_PARTITION=y 17 | CONFIG_BUILD_OUTPUT_UF2=y 18 | 19 | CONFIG_MPU_ALLOW_FLASH_WRITE=y 20 | CONFIG_NVS=y 21 | CONFIG_SETTINGS_NVS=y 22 | CONFIG_FLASH=y 23 | CONFIG_FLASH_PAGE_LAYOUT=y 24 | CONFIG_FLASH_MAP=y -------------------------------------------------------------------------------- /examples/custom_board/boards/arm/nice_nano/pre_dt_board.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 The ZMK Contributors 3 | # SPDX-License-Identifier: MIT 4 | # 5 | 6 | # Suppresses duplicate unit-address warning at build time for power, clock, acl and flash-controller 7 | # https://docs.zephyrproject.org/latest/build/dts/intro-input-output.html 8 | 9 | list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled") -------------------------------------------------------------------------------- /examples/custom_board/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | # Because this board is not supported by zephyr yet, we need to provide its whole definition by ourselves, this can be found in the "boards" folder. 3 | # The details on the capability of supporting custom boards can be found at: https://docs.zephyrproject.org/latest/hardware/porting/board_porting.html 4 | board: nice_nano_v2 5 | 6 | board: 7 | bootloader: adafruit_nrf52_sd140_v6 8 | i2c: 9 | - id: i2c0 10 | 11 | factory_reset_button: btn1 12 | 13 | buttons: 14 | - id: btn1 15 | pin: 1.15 16 | 17 | sensors: 18 | - type: scd4x 19 | i2c: 20 | id: i2c0 21 | -------------------------------------------------------------------------------- /examples/debug/README.md: -------------------------------------------------------------------------------- 1 | ## Debug configuration 2 | 3 | Enables simple, more debug-friendly configuration, that will provide more information to the user on the behavior. 4 | 5 | This includes 6 | - LEDs that will show power & connection state 7 | - USB interface that will print logs. 8 | 9 | Configuration defines `led1` as "forwarded" from the board configuration. `led2` is defined as LED connected to pin `0.04`. 10 | 11 | ### Reading USB logs 12 | 13 | On Linux(tested on Ubuntu) the logs can be read using programs like `screen` and `minicom`. 14 | 15 | For example with `minicom` the logs can be read with: `sudo minicom -D /dev/ttyACM0 115200`. `ACM0` can be different on your machine depending on the configuration and connected devices. 16 | Available `ACM` interfaces can be checked with `ls /dev/ttyA*`. When board is connected - one of the `ACM` devices will be boards logging output. -------------------------------------------------------------------------------- /examples/debug/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: arduino_nano_33_ble 3 | 4 | board: 5 | bootloader: adafruit_nrf52_sd140_v6 6 | debug: 7 | enabled: false 8 | console: usb 9 | leds: 10 | enabled: true 11 | power: led1 12 | connection: led2 13 | leds: 14 | - id: led1 15 | - id: led2 16 | pin: 0.04 -------------------------------------------------------------------------------- /examples/examples_test.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/ffenix113/zigbee_home/cmd/zigbee/firmware" 10 | "github.com/ffenix113/zigbee_home/config" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | // This test will just ensure that the firmwares can be generated. 15 | // It does not test that they are valid or can be built. 16 | // 17 | // Still, it is useful to see possible broken configurations, 18 | // validate that templates and required functions. 19 | func TestGenerateAllExamples(t *testing.T) { 20 | cwdAbsPath, err := filepath.Abs(".") 21 | require.NoError(t, err) 22 | 23 | examples, err := os.ReadDir(cwdAbsPath) 24 | require.NoError(t, err) 25 | 26 | for _, example := range examples { 27 | if !example.IsDir() { 28 | continue 29 | } 30 | 31 | t.Run(example.Name(), func(t *testing.T) { 32 | runExample(t, filepath.Join(cwdAbsPath, example.Name())) 33 | }) 34 | } 35 | } 36 | 37 | // This test will generate the configuration from the repository root. 38 | // Hopefuly resulting in less issues and up-to-date configuration. 39 | func TestGenerateRootConfiguration(t *testing.T) { 40 | cwdAbsPath, err := filepath.Abs("./../") 41 | require.NoError(t, err) 42 | 43 | runExample(t, cwdAbsPath) 44 | } 45 | 46 | func runExample(t *testing.T, exampleDir string) { 47 | // FIXME: This is needed for config parser to 48 | // resolve includes in config files. 49 | os.Chdir(exampleDir) 50 | 51 | tmpDir, err := os.MkdirTemp("", "zigbee_home_*") 52 | require.NoError(t, err) 53 | 54 | t.Logf("created %s for example %s", tmpDir, exampleDir) 55 | 56 | t.Cleanup(func() { 57 | os.RemoveAll(tmpDir) 58 | }) 59 | 60 | configPath := filepath.Join(exampleDir, "zigbee.yaml") 61 | // It is possible that examples would have some configuration 62 | // with non-default configuration file name. 63 | // If we will find this - skip such example. 64 | if _, err := os.Stat(configPath); err != nil { 65 | t.Skipf("skipping, as cannot access config file: %s", err.Error()) 66 | } 67 | 68 | cfg, err := config.ParseFromFile(configPath) 69 | require.NoError(t, err) 70 | 71 | err = firmware.GenerateFirmwareFiles(context.Background(), tmpDir, false, cfg) 72 | require.NoError(t, err) 73 | } 74 | -------------------------------------------------------------------------------- /examples/factory_reset/README.md: -------------------------------------------------------------------------------- 1 | ## Factory reset button 2 | 3 | This example demonstrates how to set up custom factory reset button. 4 | 5 | Note: if factory reset button is not defined in the configuration it will be the first button available in board definition. For example for nRF52840 Dongle it would be [this](https://github.com/zephyrproject-rtos/zephyr/blob/453ab8a9a356acf475a965a777a370795effa255/boards/nordic/nrf52840dongle/nrf52840dongle_nrf52840.dts#L64) button, for Adafruit Feather nRF52840 Express it would be [this](https://github.com/zephyrproject-rtos/zephyr/blob/453ab8a9a356acf475a965a777a370795effa255/boards/adafruit/feather/adafruit_feather_nrf52840.dts#L43) button. 6 | 7 | If board definition does not have any buttons and configuration does not define one - factory reset functionality through button press will not be available. In this case to reset Zigbee connection information it is needed to erase the device. 8 | 9 | To properly test this example the board should be connected to a Zigbee network, and then the factory reset procedure(see `Usage`) should be done. 10 | 11 | ### Explanation 12 | Factory reset will allow device to "forget" current network and start searching for new network. 13 | 14 | For example when network was changed(encryption, channel, etc), but device still expects old network to be available. 15 | This will result in a situation where device will never connect to a network that it has stored in its memory. 16 | 17 | ### Usage 18 | 19 | To actually trigger factory reset the user must hold the factory reset button for at least 5 seconds. 20 | If debug & LEDs are enabled - LED for connection should then turn off and device automatically will start trying to connect to any open Zigbee networks. -------------------------------------------------------------------------------- /examples/factory_reset/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: nrf52840dongle_nrf52840 3 | 4 | board: 5 | # `btn1` is a reference to any button defined in `board.buttons` via its `id`. 6 | factory_reset_button: btn1 7 | 8 | buttons: 9 | # This could be either button that already defined in the board(in that case `pin` definition can be omited, and specify only `id`), 10 | # or user can define any custom pin to be a button. 11 | # For custom pin both `id` and `pin` must be defined. 12 | - id: btn1 13 | # For this example random pin 0.13 is chosen to be a button. 14 | pin: 0.13 15 | # `button0` is defined in board definition here: https://github.com/zephyrproject-rtos/zephyr/blob/453ab8a9a356acf475a965a777a370795effa255/boards/nordic/nrf52840dongle/nrf52840dongle_nrf52840.dts#L64 16 | # User can choose any button from their board definition. 17 | - id: button0 -------------------------------------------------------------------------------- /examples/sensor_bme280/README.md: -------------------------------------------------------------------------------- 1 | ## A board with Bosch BME280 sensor attached 2 | 3 | Simple example with one BME280 sensor attached and using I2C interface. -------------------------------------------------------------------------------- /examples/sensor_bme280/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: nrf52840dongle_nrf52840 3 | 4 | board: 5 | i2c: 6 | # ID of instance is the same as defined in the SoC definition. 7 | # Generally they are in form of `i2c[0-9]`. 8 | # Number of i2c instances is also limited, and this allows only to 9 | # re-define pins for specified i2c instance. 10 | - id: i2c0 11 | # Pins can also be defined, if want to re-bind I2C to another ones. 12 | # sda: 0.29 13 | # scl: 14 | # port: 0 15 | # pin: 31 16 | 17 | sensors: 18 | - type: bme280 19 | i2c: 20 | # I2C instance ID. Should be defined in Zephyr's board definition. 21 | id: i2c0 22 | # Default address for BME280. 23 | addr: '0x76' 24 | -------------------------------------------------------------------------------- /examples/sensor_dht/README.md: -------------------------------------------------------------------------------- 1 | ## A board with Aosong DHT sensor attached 2 | 3 | Simple example with one DHT sensor attached and using 1-wire interface (GPIO). 4 | -------------------------------------------------------------------------------- /examples/sensor_dht/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: nrf52840dongle_nrf52840 3 | 4 | board: 5 | 6 | sensors: 7 | # Add a dht sensor from aosong manufacturer 8 | # If DHT22 or AM2302 is used, variant must be set to "dht22". DHT11 does not need variant. 9 | - type: dht 10 | variant: "dht22" 11 | pin: 12 | port: 1 13 | pin: 13 14 | -------------------------------------------------------------------------------- /examples/sensor_scd4x/README.md: -------------------------------------------------------------------------------- 1 | ## A board with Sensirion SCD4X sensor attached via I2C 2 | 3 | Simple example with one SCD4X sensor attached and using I2C. 4 | -------------------------------------------------------------------------------- /examples/sensor_scd4x/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: nrf52840dongle_nrf52840 3 | 4 | board: 5 | i2c: 6 | # Only ID can be specified if interface is present in board configuration. 7 | - id: i2c0 8 | 9 | sensors: 10 | # Add a SCD4X sensor from Sensirion manufacturer. 11 | - type: scd4x 12 | i2c: 13 | id: i2c0 14 | # This option is defined here: 15 | # https://github.com/nobodyguy/sensirion_zephyr_drivers/blob/438ae62df66e9b1d638e0d4eb1c3cf6ad2ae449b/dts/bindings/sensor/sensirion%2Cscd4x.yaml#L51 16 | # This parameter is optional and can be omitted. 17 | temperature_offset: 2 18 | -------------------------------------------------------------------------------- /examples/set_toolchain_version/README.md: -------------------------------------------------------------------------------- 1 | ## Set toolchain version 2 | 3 | User can provide specific toolchain version that should be used. 4 | 5 | By default, if it is not defined explicitely, the default version from 6 | toolchain configuration will be used. 7 | 8 | If version specified in the configuration file is not available - 9 | next available patch version will be selected. 10 | 11 | Zigbee_home has default version of toolchain specified, 12 | that will be selected if no other version is specified 13 | in configuration file or environment. 14 | This default version is defined in `config/device.go`. -------------------------------------------------------------------------------- /examples/set_toolchain_version/zigbee.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | board: nrf52840dongle_nrf52840 3 | # We can force specific version of toolchain (if it is available). 4 | ncs_version: v2.6.2 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ffenix113/zigbee_home 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/stretchr/testify v1.9.0 7 | github.com/urfave/cli/v2 v2.27.3 8 | gopkg.in/yaml.v3 v3.0.1 9 | ) 10 | 11 | require ( 12 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 16 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 10 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 11 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 12 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 13 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 14 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 15 | github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI= 16 | github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 17 | github.com/urfave/cli/v2 v2.27.3 h1:/POWahRmdh7uztQ3CYnaDddk0Rm90PyOgIxgW2rr41M= 18 | github.com/urfave/cli/v2 v2.27.3/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= 19 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 20 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 21 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 22 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 26 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Zigbee_home 2 | theme: 3 | name: material 4 | language: en 5 | features: 6 | - navigation.indexes 7 | 8 | repo_url: https://github.com/ffenix113/zigbee_home 9 | repo_name: ffenix113/zigbee_home 10 | 11 | markdown_extensions: 12 | # Definition lists (https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown/#definition-lists) 13 | - def_list 14 | # Admonitions (https://squidfunk.github.io/mkdocs-material/reference/admonitions/) 15 | - admonition 16 | - pymdownx.details 17 | - pymdownx.superfences 18 | # Emojis 19 | - attr_list 20 | - pymdownx.emoji: 21 | emoji_index: !!python/name:material.extensions.emoji.twemoji 22 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 23 | # Permalinks for page headings 24 | - toc: 25 | permalink: true 26 | -------------------------------------------------------------------------------- /sensor/aosong/dht.go: -------------------------------------------------------------------------------- 1 | package aosong 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ffenix113/zigbee_home/sensor/base" 7 | "github.com/ffenix113/zigbee_home/templates/extenders" 8 | "github.com/ffenix113/zigbee_home/types/appconfig" 9 | dt "github.com/ffenix113/zigbee_home/types/devicetree" 10 | "github.com/ffenix113/zigbee_home/types/generator" 11 | "github.com/ffenix113/zigbee_home/zcl/cluster" 12 | "github.com/ffenix113/zigbee_home/types" 13 | ) 14 | 15 | type DHT struct { 16 | *base.Base `yaml:",inline"` 17 | Pin types.Pin 18 | Variant string `yaml:"variant"` 19 | } 20 | 21 | func NewDHT() *DHT { 22 | return &DHT{ 23 | Variant: "", 24 | } 25 | } 26 | 27 | func (b DHT) String() string { 28 | return "Aosong" 29 | } 30 | 31 | func (DHT) Clusters() cluster.Clusters { 32 | return []cluster.Cluster{ 33 | cluster.Temperature{ 34 | MinMeasuredValue: -40, 35 | MaxMeasuredValue: 80, 36 | Tolerance: 1, 37 | }, 38 | cluster.NewRelativeHumidity(0, 100), 39 | } 40 | } 41 | 42 | func (b DHT) AppConfig() []appconfig.ConfigValue { 43 | return []appconfig.ConfigValue{ 44 | appconfig.NewValue("CONFIG_GPIO").Required(appconfig.Yes), 45 | appconfig.CONFIG_DHT.Required(appconfig.Yes), 46 | } 47 | } 48 | 49 | func (b DHT) ApplyOverlay(tree *dt.DeviceTree) error { 50 | 51 | pinctrlNode := tree.FindSpecificNode(dt.SearchByLabel(dt.NodeLabelPinctrl)) 52 | if pinctrlNode == nil { 53 | return dt.ErrNodeNotFound(dt.NodeLabelPinctrl) 54 | } 55 | 56 | pinLabel := fmt.Sprintf("gpio%d %d (GPIO_PULL_UP | GPIO_ACTIVE_LOW)", b.Pin.Port, b.Pin.Pin) 57 | 58 | props:= []dt.Property{ 59 | dt.NewProperty("compatible", dt.FromValue("aosong,dht")), 60 | dt.NewProperty("status", dt.FromValue("okay")), 61 | dt.NewProperty("dio-gpios", dt.Angled(dt.Label(pinLabel))), 62 | } 63 | // Add variant property only if Variant is not emtpy 64 | // For dht22 or AM2302 devices the string "dht22;" must be added to device tree overlay 65 | // See : https://docs.zephyrproject.org/latest/build/dts/api/bindings/sensor/aosong,dht.html 66 | if b.Variant != "" { 67 | props = append(props, dt.NewProperty(b.Variant, nil)) 68 | } 69 | 70 | pinctrlNode.AddNodes( 71 | &dt.Node{ 72 | Name: "dht22", 73 | Label: b.Label(), 74 | Properties: props, 75 | }, 76 | ) 77 | 78 | return nil 79 | } 80 | 81 | func (DHT) Extenders() []generator.Extender { 82 | return []generator.Extender{ 83 | extenders.NewSensor(), 84 | extenders.GPIO{}, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /sensor/base/base.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/types/appconfig" 5 | "github.com/ffenix113/zigbee_home/types/devicetree" 6 | "github.com/ffenix113/zigbee_home/zcl/cluster" 7 | ) 8 | 9 | // SensorType is type that will fetch only 10 | type SensorType struct { 11 | Type string 12 | } 13 | 14 | // Sensor defines all information necessary about the attached sensor. 15 | type Base struct { 16 | Type string 17 | label string 18 | // Connection provides information about communication protocol. 19 | Connection map[string]string 20 | } 21 | 22 | func (b *Base) Label() string { 23 | return b.label 24 | } 25 | 26 | func (b *Base) SetLabel(label string) { 27 | if b.label != "" { 28 | panic("trying to set label second time") 29 | } 30 | 31 | b.label = label 32 | } 33 | 34 | func (*Base) Template() string { 35 | return "" 36 | } 37 | 38 | func (*Base) Clusters() cluster.Clusters { 39 | return nil 40 | } 41 | 42 | func (*Base) AppConfig() []appconfig.ConfigValue { 43 | return nil 44 | } 45 | 46 | func (*Base) ApplyOverlay(overlay *devicetree.DeviceTree) error { 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /sensor/base/common.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/zcl/cluster" 5 | ) 6 | 7 | type CommonDeviceClusters struct { 8 | *Base `yaml:",inline"` 9 | } 10 | 11 | func NewCommonDeviceClusters() *CommonDeviceClusters { 12 | return &CommonDeviceClusters{ 13 | Base: &Base{ 14 | label: "common_device_clusters", 15 | }, 16 | } 17 | } 18 | 19 | func (*CommonDeviceClusters) String() string { 20 | return "Common device clusters" 21 | } 22 | 23 | func (*CommonDeviceClusters) Template() string { 24 | return "sensors/common" 25 | } 26 | 27 | func (o *CommonDeviceClusters) Clusters() cluster.Clusters { 28 | return []cluster.Cluster{ 29 | cluster.Basic{}, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sensor/base/ias_zone.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/zcl/cluster" 5 | ) 6 | 7 | func NewContact() *IASZone { 8 | return &IASZone{ 9 | ZoneType: cluster.IasZoneContact, 10 | } 11 | } 12 | 13 | type IASZone struct { 14 | *Base `yaml:",inline"` 15 | Button string `yaml:"button"` 16 | ZoneType cluster.IasZoneType `yaml:"zone_type"` 17 | } 18 | 19 | func (*IASZone) String() string { 20 | return "IAS Zone" 21 | } 22 | 23 | func (*IASZone) Template() string { 24 | return "sensors/ias_zone" 25 | } 26 | 27 | func (z *IASZone) Clusters() cluster.Clusters { 28 | // By default - be contact sensor for now. 29 | if z.ZoneType == "" { 30 | z.ZoneType = cluster.IasZoneContact 31 | } 32 | 33 | return []cluster.Cluster{ 34 | cluster.IASZone{ZoneType: z.ZoneType}, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sensor/base/on_off.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/templates/extenders" 5 | "github.com/ffenix113/zigbee_home/types" 6 | "github.com/ffenix113/zigbee_home/types/appconfig" 7 | "github.com/ffenix113/zigbee_home/types/devicetree" 8 | "github.com/ffenix113/zigbee_home/types/generator" 9 | "github.com/ffenix113/zigbee_home/zcl/cluster" 10 | ) 11 | 12 | type OnOff struct { 13 | *Base `yaml:",inline"` 14 | Pin types.Pin 15 | } 16 | 17 | func (*OnOff) String() string { 18 | return "On/Off" 19 | } 20 | 21 | func (*OnOff) Template() string { 22 | return "sensors/on_off" 23 | } 24 | 25 | func (o *OnOff) Clusters() cluster.Clusters { 26 | return []cluster.Cluster{ 27 | cluster.OnOff{PinLabel: o.Pin.Label()}, 28 | } 29 | } 30 | 31 | func (*OnOff) AppConfig() []appconfig.ConfigValue { 32 | return []appconfig.ConfigValue{ 33 | appconfig.NewValue("CONFIG_GPIO").Required(appconfig.Yes), 34 | } 35 | } 36 | 37 | func (o *OnOff) ApplyOverlay(overlay *devicetree.DeviceTree) error { 38 | dtPin := devicetree.NewLED(o.Pin) 39 | return dtPin.AttachSelf(overlay) 40 | } 41 | 42 | func (*OnOff) Extenders() []generator.Extender { 43 | return []generator.Extender{ 44 | extenders.GPIO{}, 45 | extenders.NewLEDs(), 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sensor/base/power_config.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/templates/extenders" 5 | "github.com/ffenix113/zigbee_home/types/appconfig" 6 | "github.com/ffenix113/zigbee_home/types/devicetree" 7 | "github.com/ffenix113/zigbee_home/types/generator" 8 | "github.com/ffenix113/zigbee_home/zcl/cluster" 9 | ) 10 | 11 | type PowerConfiguration struct { 12 | *Base `yaml:",inline"` 13 | cluster.PowerConfiguration `yaml:",inline"` 14 | ADCPin devicetree.ADCPin `yaml:"adc_pin"` 15 | } 16 | 17 | func (*PowerConfiguration) String() string { 18 | return "PowerConfiguration" 19 | } 20 | 21 | func (*PowerConfiguration) Template() string { 22 | return "sensors/power_config" 23 | } 24 | 25 | func (o *PowerConfiguration) Clusters() cluster.Clusters { 26 | clusterConfig := o.PowerConfiguration 27 | clusterConfig.BatteryRatedVoltage /= 100 28 | clusterConfig.BatteryVoltageMinThreshold /= 100 29 | return []cluster.Cluster{ 30 | clusterConfig, 31 | } 32 | } 33 | 34 | func (*PowerConfiguration) AppConfig() []appconfig.ConfigValue { 35 | return []appconfig.ConfigValue{ 36 | appconfig.NewValue("CONFIG_ADC").Required(appconfig.Yes), 37 | } 38 | } 39 | 40 | func (o *PowerConfiguration) ApplyOverlay(overlay *devicetree.DeviceTree) error { 41 | dtPin := devicetree.NewButton(o.ADCPin.Pin) 42 | return dtPin.AttachSelf(overlay) 43 | } 44 | 45 | func (c *PowerConfiguration) Extenders() []generator.Extender { 46 | return []generator.Extender{ 47 | extenders.NewADC(c.ADCPin), 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sensor/base/soil_moisture_adc.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/templates/extenders" 5 | "github.com/ffenix113/zigbee_home/types/appconfig" 6 | "github.com/ffenix113/zigbee_home/types/devicetree" 7 | "github.com/ffenix113/zigbee_home/types/generator" 8 | "github.com/ffenix113/zigbee_home/zcl/cluster" 9 | ) 10 | 11 | type SoilMoistureADC struct { 12 | *Base `yaml:",inline"` 13 | MinMoistureMv uint16 `yaml:"min_moisture_mv"` 14 | MaxMoistureMv uint16 `yaml:"max_moisture_mv"` 15 | ADCPin devicetree.ADCPin `yaml:"adc_pin"` 16 | } 17 | 18 | func (*SoilMoistureADC) String() string { 19 | return "SoilMoistureADC" 20 | } 21 | 22 | func (*SoilMoistureADC) Template() string { 23 | return "sensors/soil_moisture_adc" 24 | } 25 | 26 | func (o *SoilMoistureADC) Clusters() cluster.Clusters { 27 | return []cluster.Cluster{ 28 | // Hardcoded, as we don't configure this values. 29 | cluster.NewSoilMoisture(0, 100), 30 | } 31 | } 32 | 33 | func (*SoilMoistureADC) AppConfig() []appconfig.ConfigValue { 34 | return []appconfig.ConfigValue{ 35 | appconfig.NewValue("CONFIG_ADC").Required(appconfig.Yes), 36 | } 37 | } 38 | 39 | func (o *SoilMoistureADC) ApplyOverlay(overlay *devicetree.DeviceTree) error { 40 | dtPin := devicetree.NewButton(o.ADCPin.Pin) 41 | return dtPin.AttachSelf(overlay) 42 | } 43 | 44 | func (c *SoilMoistureADC) Extenders() []generator.Extender { 45 | return []generator.Extender{ 46 | extenders.NewADC(c.ADCPin), 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sensor/base/types.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import "strings" 4 | 5 | type I2CConnection struct { 6 | ID string 7 | Addr string 8 | } 9 | 10 | func (c I2CConnection) UnitAddress() string { 11 | if strings.HasPrefix(c.Addr, "0x") { 12 | return c.Addr[2:] 13 | } 14 | 15 | return c.Addr 16 | } 17 | 18 | func (c I2CConnection) Reg() string { 19 | if !strings.HasPrefix(c.Addr, "0x") { 20 | return "0x" + c.Addr 21 | } 22 | 23 | return c.Addr 24 | } 25 | -------------------------------------------------------------------------------- /sensor/bosch/bme280.go: -------------------------------------------------------------------------------- 1 | package bosch 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/ffenix113/zigbee_home/sensor/base" 7 | "github.com/ffenix113/zigbee_home/templates/extenders" 8 | "github.com/ffenix113/zigbee_home/types/appconfig" 9 | dt "github.com/ffenix113/zigbee_home/types/devicetree" 10 | "github.com/ffenix113/zigbee_home/types/generator" 11 | "github.com/ffenix113/zigbee_home/zcl/cluster" 12 | ) 13 | 14 | type BME280 struct { 15 | *base.Base `yaml:",inline"` 16 | I2C base.I2CConnection 17 | Variant string `yaml:"-"` 18 | } 19 | 20 | func NewBME280() *BME280 { 21 | return &BME280{ 22 | Variant: "bme280", 23 | } 24 | } 25 | 26 | func (b BME280) String() string { 27 | return "Bosch " + strings.ToUpper(b.Variant) 28 | } 29 | 30 | func (BME280) Clusters() cluster.Clusters { 31 | return []cluster.Cluster{ 32 | cluster.Temperature{ 33 | MinMeasuredValue: -40, 34 | MaxMeasuredValue: 85, 35 | Tolerance: 1, 36 | }, 37 | cluster.Pressure{ 38 | MinMeasuredValue: 30, 39 | MaxMeasuredValue: 110, 40 | Tolerance: 0, 41 | }, 42 | cluster.NewRelativeHumidity(10, 90), 43 | } 44 | } 45 | 46 | func (b BME280) AppConfig() []appconfig.ConfigValue { 47 | return []appconfig.ConfigValue{ 48 | appconfig.CONFIG_I2C.Required(appconfig.Yes), 49 | // Yes, for both 280 & 680 we are setting through BME280 50 | appconfig.CONFIG_BME280.Required(appconfig.Yes), 51 | appconfig.NewValue("CONFIG_BME280_MODE_FORCED").Required(appconfig.Yes), 52 | } 53 | } 54 | 55 | func (b BME280) ApplyOverlay(tree *dt.DeviceTree) error { 56 | i2cNode := tree.FindSpecificNode(dt.SearchByLabel(b.I2C.ID)) 57 | if i2cNode == nil { 58 | return dt.ErrNodeNotFound(b.I2C.ID) 59 | } 60 | 61 | i2cNode.AddNodes(&dt.Node{ 62 | Name: b.Variant, 63 | Label: b.Label(), 64 | UnitAddress: b.I2C.UnitAddress(), 65 | Properties: []dt.Property{ 66 | dt.NewProperty(dt.PropertyNameCompatible, dt.FromValue("bosch,"+b.Variant)), 67 | dt.NewProperty("reg", dt.Angled(dt.String(b.I2C.Reg()))), 68 | dt.PropertyStatusEnable, 69 | }, 70 | }) 71 | 72 | return nil 73 | } 74 | 75 | func (BME280) Extenders() []generator.Extender { 76 | return []generator.Extender{ 77 | extenders.NewSensor(), 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /sensor/bosch/bme680.go: -------------------------------------------------------------------------------- 1 | package bosch 2 | 3 | func NewBME680() *BME280 { 4 | return &BME280{ 5 | Variant: "bme680", 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sensor/device_temperature.go: -------------------------------------------------------------------------------- 1 | package sensor 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/sensor/base" 5 | "github.com/ffenix113/zigbee_home/templates/extenders" 6 | "github.com/ffenix113/zigbee_home/types/appconfig" 7 | "github.com/ffenix113/zigbee_home/types/generator" 8 | "github.com/ffenix113/zigbee_home/zcl/cluster" 9 | ) 10 | 11 | var _ appconfig.Provider = &DeviceTemperature{} 12 | 13 | type DeviceTemperature struct { 14 | *base.Base `yaml:",inline"` 15 | } 16 | 17 | func (*DeviceTemperature) String() string { 18 | return "device temperature" 19 | } 20 | 21 | func (*DeviceTemperature) AppConfig() []appconfig.ConfigValue { 22 | return []appconfig.ConfigValue{ 23 | appconfig.NewValue("CONFIG_NRFX_TEMP").Required(appconfig.Yes), 24 | } 25 | } 26 | 27 | func (*DeviceTemperature) Clusters() cluster.Clusters { 28 | return []cluster.Cluster{ 29 | cluster.DeviceTemperature{}, 30 | } 31 | } 32 | 33 | func (*DeviceTemperature) Template() string { 34 | return "sensors/device_temperature" 35 | } 36 | 37 | func (*DeviceTemperature) Extenders() []generator.Extender { 38 | return []generator.Extender{ 39 | extenders.NewNrfxTemp(), 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sensor/sensirion/scd4x.go: -------------------------------------------------------------------------------- 1 | package sensirion 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/sensor/base" 5 | "github.com/ffenix113/zigbee_home/templates/extenders" 6 | "github.com/ffenix113/zigbee_home/types/appconfig" 7 | dt "github.com/ffenix113/zigbee_home/types/devicetree" 8 | "github.com/ffenix113/zigbee_home/types/generator" 9 | "github.com/ffenix113/zigbee_home/zcl/cluster" 10 | ) 11 | 12 | type SCD4X struct { 13 | *base.Base `yaml:",inline"` 14 | I2C base.I2CConnection 15 | TemperatureOffset int8 `yaml:"temperature_offset"` 16 | } 17 | 18 | func (SCD4X) String() string { 19 | // TODO: Update to SCD4X when will support properly SCD40 20 | return "Sensirion SCD4X (SCD41)" 21 | } 22 | 23 | func (SCD4X) Clusters() cluster.Clusters { 24 | // https://sensirion.com/media/documents/E0F04247/631EF271/CD_DS_SCD40_SCD41_Datasheet_D1.pdf 25 | return []cluster.Cluster{ 26 | cluster.Temperature{ 27 | MinMeasuredValue: -10, 28 | MaxMeasuredValue: 60, 29 | Tolerance: 1, 30 | }, 31 | cluster.NewRelativeHumidity(0, 100), 32 | cluster.CarbonDioxide{ 33 | MinMeasuredValue: 400, 34 | MaxMeasuredValue: 5000, 35 | Tolerance: 40, 36 | }, 37 | } 38 | } 39 | 40 | func (SCD4X) AppConfig() []appconfig.ConfigValue { 41 | return []appconfig.ConfigValue{ 42 | appconfig.CONFIG_I2C.Required(appconfig.Yes), 43 | appconfig.NewValue("CONFIG_CRC").Required(appconfig.Yes), 44 | appconfig.NewValue("CONFIG_SCD4X").Required(appconfig.Yes), 45 | } 46 | } 47 | 48 | func (s SCD4X) ApplyOverlay(tree *dt.DeviceTree) error { 49 | i2cNode := tree.FindSpecificNode(dt.SearchByLabel(s.I2C.ID)) 50 | if i2cNode == nil { 51 | return dt.ErrNodeNotFound(s.I2C.ID) 52 | } 53 | // SCD4X address is static 54 | s.I2C.Addr = "0x62" 55 | 56 | i2cNode.AddNodes(&dt.Node{ 57 | Name: "scd4x", 58 | Label: s.Label(), 59 | UnitAddress: s.I2C.UnitAddress(), 60 | Properties: []dt.Property{ 61 | dt.PropertyStatusEnable, 62 | dt.NewProperty(dt.PropertyNameCompatible, dt.FromValue("sensirion,scd4x")), 63 | dt.NewProperty("reg", dt.Angled(dt.String(s.I2C.Reg()))), 64 | // Only single-shot for now. 65 | // Would need some changes in templates for changing 66 | dt.NewProperty("measure-mode", dt.FromValue("single-shot")), 67 | dt.NewProperty("model", dt.FromValue("scd41")), 68 | dt.NewProperty("temperature-offset", dt.FromValue(s.TemperatureOffset)), 69 | }, 70 | }) 71 | 72 | return nil 73 | } 74 | 75 | func (s SCD4X) Extenders() []generator.Extender { 76 | return []generator.Extender{ 77 | extenders.NewSensor(), 78 | extenders.NewSCD4X(), 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /templates/extenders/adc.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | 7 | "github.com/ffenix113/zigbee_home/types/devicetree" 8 | "github.com/ffenix113/zigbee_home/types/generator" 9 | ) 10 | 11 | var _ generator.Extender = ADC{} 12 | var _ devicetree.Applier = ADC{} 13 | 14 | type ADC struct { 15 | generator.SimpleExtender 16 | 17 | Instances []devicetree.ADCPin 18 | } 19 | 20 | func NewADC(instances ...devicetree.ADCPin) generator.Extender { 21 | return ADC{ 22 | Instances: instances, 23 | } 24 | } 25 | 26 | func (l ADC) Template() string { 27 | return path.Join("peripherals", "adc") 28 | } 29 | 30 | func (l ADC) WriteFiles() []generator.WriteFile { 31 | return []generator.WriteFile{ 32 | { 33 | FileName: "adc.c", 34 | TemplateName: "adc.c", 35 | }, 36 | { 37 | FileName: "adc.h", 38 | TemplateName: "adc.h", 39 | }, 40 | } 41 | } 42 | 43 | func (l ADC) Includes() []string { 44 | return []string{"zephyr/drivers/adc.h", "adc.h"} 45 | } 46 | 47 | func (l ADC) ApplyOverlay(dt *devicetree.DeviceTree) error { 48 | for _, instance := range l.Instances { 49 | if err := instance.AttachSelf(dt); err != nil { 50 | return fmt.Errorf("attach adc: %w", err) 51 | } 52 | } 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /templates/extenders/bootloader.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/types/board" 5 | "github.com/ffenix113/zigbee_home/types/generator" 6 | ) 7 | 8 | func NewBootloaderConfig(config *board.Bootloader) generator.SimpleExtender { 9 | extender := generator.SimpleExtender{ 10 | Name: "Bootloader configuration", 11 | Config: config.Config, 12 | } 13 | 14 | if config.PM != nil { 15 | extender.FilesToWrite = []generator.WriteFile{ 16 | { 17 | FileName: "../pm_static.yml", 18 | TemplateName: "pm_static.yml", 19 | AdditionalContext: config.PM, 20 | }, 21 | } 22 | } 23 | 24 | return extender 25 | } 26 | -------------------------------------------------------------------------------- /templates/extenders/buttons.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "path" 7 | 8 | "github.com/ffenix113/zigbee_home/types" 9 | "github.com/ffenix113/zigbee_home/types/devicetree" 10 | "github.com/ffenix113/zigbee_home/types/generator" 11 | ) 12 | 13 | var _ generator.Extender = Button{} 14 | var _ devicetree.Applier = Button{} 15 | 16 | type Button struct { 17 | generator.SimpleExtender 18 | 19 | Instances []types.Pin 20 | } 21 | 22 | func NewButtons(instances ...types.Pin) generator.Extender { 23 | for i := range instances { 24 | if instances[i].ID == "" { 25 | log.Fatalf("button %#v must have an id set", instances[i]) 26 | } 27 | } 28 | 29 | return Button{ 30 | Instances: instances, 31 | } 32 | } 33 | 34 | func (b Button) Template() string { 35 | return path.Join("peripherals", "buttons") 36 | } 37 | 38 | func (b Button) Includes() []string { 39 | return []string{"zephyr/drivers/gpio.h"} 40 | } 41 | 42 | func (b Button) ApplyOverlay(dt *devicetree.DeviceTree) error { 43 | for _, instance := range b.Instances { 44 | ledInstance := devicetree.NewButton(instance) 45 | if err := ledInstance.AttachSelf(dt); err != nil { 46 | return fmt.Errorf("attach button: %w", err) 47 | } 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /templates/extenders/debug_log.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/ffenix113/zigbee_home/types/appconfig" 7 | "github.com/ffenix113/zigbee_home/types/devicetree" 8 | "github.com/ffenix113/zigbee_home/types/generator" 9 | ) 10 | 11 | const DebugConsoleUSB = "usb" 12 | 13 | type DebugLEDs struct { 14 | Enabled bool 15 | Power string 16 | Connection string 17 | } 18 | 19 | type DebugConfig struct { 20 | Enabled bool 21 | LEDs DebugLEDs 22 | Console string 23 | } 24 | 25 | // Debug log optimizations 26 | // CONFIG_DEBUG_OPTIMIZATIONS=y 27 | // CONFIG_DEBUG_THREAD_INFO=y 28 | 29 | func NewDebugUARTLog(config DebugConfig) generator.Extender { 30 | if !config.Enabled { 31 | return nil 32 | } 33 | 34 | if config.Console == "" { 35 | panic("console value must be set for debug configuration") 36 | } 37 | 38 | if strings.HasPrefix(config.Console, DebugConsoleUSB) && config.Console != DebugConsoleUSB { 39 | panic("debug backend should be 'usb', but is " + config.Console) 40 | } 41 | 42 | ledsEnabled := appconfig.Yes 43 | if !config.LEDs.Enabled { 44 | ledsEnabled = appconfig.No 45 | } 46 | 47 | return generator.SimpleExtender{ 48 | IncludeHeaders: []string{ 49 | "zephyr/logging/log.h", 50 | "zephyr/drivers/uart.h", 51 | }, 52 | Config: []appconfig.ConfigValue{ 53 | // Logging setup 54 | appconfig.CONFIG_LOG.Required(appconfig.Yes), 55 | appconfig.CONFIG_CONSOLE.Required(appconfig.Yes), 56 | appconfig.CONFIG_SERIAL.Required(appconfig.Yes), 57 | appconfig.CONFIG_LOG_BACKEND_UART.Required(appconfig.Yes), 58 | appconfig.CONFIG_UART_CONSOLE.Required(appconfig.Yes), 59 | appconfig.CONFIG_UART_LINE_CTRL.Required(appconfig.Yes), 60 | appconfig.CONFIG_PRINTK.Required(appconfig.Yes), 61 | 62 | appconfig.NewValue("CONFIG_ZBOSS_HALT_ON_ASSERT").Default(appconfig.Yes), 63 | appconfig.NewValue("CONFIG_RESET_ON_FATAL_ERROR").Default(appconfig.No), 64 | appconfig.NewValue("CONFIG_DEBUG_OPTIMIZATIONS").Default(appconfig.Yes), 65 | appconfig.NewValue("CONFIG_DEBUG_THREAD_INFO").Default(appconfig.Yes), 66 | appconfig.NewValue("CONFIG_THREAD_NAME").Default(appconfig.Yes), 67 | 68 | // Configurations for (hopefully) generating 69 | // good address for addr2line on exception. 70 | appconfig.NewValue("CONFIG_DEBUG_COREDUMP").Default(appconfig.Yes), 71 | appconfig.NewValue("CONFIG_DEBUG_COREDUMP_BACKEND_LOGGING").Default(appconfig.Yes), 72 | appconfig.NewValue("CONFIG_COREDUMP_DEVICE").Default(appconfig.Yes), 73 | // appconfig.NewValue("CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE").Default(`2048`), 74 | // appconfig.NewValue("CONFIG_HEAP_MEM_POOL_SIZE").Default(`2048`), 75 | 76 | // ZBHome Debug enable 77 | appconfig.NewValue("CONFIG_ZBHOME_DEBUG_ENABLE").Required(appconfig.Yes), 78 | 79 | // Leds 80 | appconfig.NewValue("CONFIG_ZBHOME_DEBUG_LEDS").Required(ledsEnabled), 81 | 82 | // Log console 83 | appconfig.NewValue("CONFIG_ZBHOME_DEBUG_CONSOLE").Required(config.Console).Quoted(), 84 | }, 85 | OverlayFn: overlayFn(config.Console), 86 | } 87 | } 88 | 89 | func (d *DebugConfig) IsEnabled() bool { 90 | return d != nil && d.Enabled 91 | } 92 | 93 | func overlayFn(console string) func(*devicetree.DeviceTree) error { 94 | if console == DebugConsoleUSB { 95 | console = "cdc_acm_uart0" 96 | } 97 | 98 | return func(dt *devicetree.DeviceTree) error { 99 | chosen := dt.FindSpecificNode( 100 | devicetree.SearchByName(devicetree.NodeNameRoot), 101 | devicetree.SearchByName(devicetree.NodeNameChosen)) 102 | 103 | chosen.Properties = append(chosen.Properties, 104 | devicetree.NewProperty("zephyr,console", devicetree.Label(console)), 105 | devicetree.NewProperty("zephyr,shell-console", devicetree.Label(console))) 106 | 107 | return nil 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /templates/extenders/gpio.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/types/generator" 5 | ) 6 | 7 | var _ generator.Extender = GPIO{} 8 | 9 | type GPIO struct { 10 | generator.SimpleExtender `yaml:"-"` 11 | } 12 | 13 | func (GPIO) Includes() []string { 14 | return []string{"zephyr/drivers/gpio.h"} 15 | } 16 | -------------------------------------------------------------------------------- /templates/extenders/i2c.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/ffenix113/zigbee_home/types" 8 | "github.com/ffenix113/zigbee_home/types/devicetree" 9 | "github.com/ffenix113/zigbee_home/types/generator" 10 | ) 11 | 12 | var _ generator.Extender = I2C{} 13 | var _ devicetree.Applier = I2C{} 14 | 15 | type I2CInstance struct { 16 | // ID is a actual label of pre-defined I2C peripheral. 17 | // For example most SoCs would have IDs something like i2c0, i2c1, ... 18 | ID string 19 | SDA, SCL types.Pin 20 | } 21 | 22 | type I2C struct { 23 | generator.SimpleExtender 24 | 25 | Instances []I2CInstance 26 | } 27 | 28 | func NewI2C(instances ...I2CInstance) I2C { 29 | for i, instance := range instances { 30 | if len(instance.ID) != 4 || !strings.HasPrefix(instance.ID, "i2c") || (instance.ID[3] < '0' || instance.ID[3] > '9') { 31 | panic(fmt.Sprintf("i2c instance %d must have `id` format of 'i2c[0-9]'", i)) 32 | } 33 | } 34 | 35 | return I2C{ 36 | Instances: instances, 37 | } 38 | } 39 | 40 | func (i I2C) ApplyOverlay(dt *devicetree.DeviceTree) error { 41 | pinctrl := dt.FindSpecificNode(devicetree.SearchByLabel(devicetree.NodeLabelPinctrl)) 42 | 43 | for _, instance := range i.Instances { 44 | // Add pin definitions only if we have some. 45 | // Otherwise just enable the I2C instance. 46 | if instance.SDA.PinsDefined() && instance.SCL.PinsDefined() { 47 | pinctrl.AddNodes(buildI2C(instance.ID, instance)...) 48 | } 49 | 50 | dt.AddNodes(&devicetree.Node{ 51 | Label: instance.ID, 52 | Upsert: true, 53 | Properties: []devicetree.Property{devicetree.PropertyStatusEnable}, 54 | }) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func buildI2C(id string, i I2CInstance) []*devicetree.Node { 61 | return []*devicetree.Node{ 62 | { 63 | Name: id + "_default", 64 | Label: id + "_default", 65 | SubNodes: []*devicetree.Node{buildI2CNode(i, false)}, 66 | }, 67 | { 68 | Name: id + "_sleep", 69 | Label: id + "_sleep", 70 | SubNodes: []*devicetree.Node{buildI2CNode(i, true)}, 71 | }, 72 | } 73 | } 74 | 75 | func buildI2CNode(i I2CInstance, lowPowerEnable bool) *devicetree.Node { 76 | group1 := &devicetree.Node{ 77 | Name: "group1", 78 | Properties: []devicetree.Property{ 79 | devicetree.NewProperty("psels", 80 | devicetree.Array( 81 | devicetree.NrfPSel("TWIM_SDA", i.SDA.Port.Value(), i.SDA.Pin.Value()), 82 | devicetree.NrfPSel("TWIM_SCL", i.SCL.Port.Value(), i.SCL.Pin.Value()), 83 | ), 84 | ), 85 | }, 86 | } 87 | 88 | if lowPowerEnable { 89 | group1.Properties = append(group1.Properties, devicetree.NewProperty("low-power-enable", nil)) 90 | } 91 | 92 | return group1 93 | } 94 | -------------------------------------------------------------------------------- /templates/extenders/leds.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | 7 | "github.com/ffenix113/zigbee_home/types" 8 | "github.com/ffenix113/zigbee_home/types/devicetree" 9 | "github.com/ffenix113/zigbee_home/types/generator" 10 | ) 11 | 12 | var _ generator.Extender = LED{} 13 | var _ devicetree.Applier = LED{} 14 | 15 | type LED struct { 16 | generator.SimpleExtender 17 | 18 | Instances []types.Pin 19 | } 20 | 21 | func NewLEDs(instances ...types.Pin) generator.Extender { 22 | return LED{ 23 | Instances: instances, 24 | } 25 | } 26 | 27 | func (l LED) Template() string { 28 | return path.Join("peripherals", "leds") 29 | } 30 | 31 | func (l LED) Includes() []string { 32 | return []string{"zephyr/drivers/gpio.h"} 33 | } 34 | 35 | func (l LED) ApplyOverlay(dt *devicetree.DeviceTree) error { 36 | for _, instance := range l.Instances { 37 | ledInstance := devicetree.NewLED(instance) 38 | if err := ledInstance.AttachSelf(dt); err != nil { 39 | return fmt.Errorf("attach led: %w", err) 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /templates/extenders/nrfx_temp.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import "github.com/ffenix113/zigbee_home/types/generator" 4 | 5 | func NewNrfxTemp() generator.SimpleExtender { 6 | return generator.SimpleExtender{ 7 | Name: "NRFX Temp", 8 | IncludeHeaders: []string{"nrfx_temp.h"}, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/extenders/scd4x.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import "github.com/ffenix113/zigbee_home/types/generator" 4 | 5 | type SCD4X struct { 6 | generator.SimpleExtender 7 | } 8 | 9 | func NewSCD4X() SCD4X { 10 | return SCD4X{ 11 | SimpleExtender: generator.SimpleExtender{ 12 | ZephyrModuleNames: []string{"scd4x"}, 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /templates/extenders/sensor.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/types/appconfig" 5 | "github.com/ffenix113/zigbee_home/types/generator" 6 | ) 7 | 8 | var _ generator.Extender = Sensor{} 9 | var _ appconfig.Provider = Sensor{} 10 | 11 | type Sensor struct{} 12 | 13 | func NewSensor() Sensor { 14 | return Sensor{} 15 | } 16 | 17 | // AppConfig implements appconfig.Provider. 18 | func (Sensor) AppConfig() []appconfig.ConfigValue { 19 | return []appconfig.ConfigValue{ 20 | appconfig.CONFIG_SENSOR.Required(appconfig.Yes), 21 | } 22 | } 23 | 24 | // Includes implements templates.Extender. 25 | func (Sensor) Includes() []string { 26 | return []string{"zephyr/drivers/sensor.h", "zbhome_sensor.hpp"} 27 | } 28 | 29 | // Template implements templates.Extender. 30 | func (Sensor) Template() string { 31 | return "" 32 | } 33 | 34 | // WriteFiles implements templates.Extender. 35 | func (Sensor) WriteFiles() []generator.WriteFile { 36 | return []generator.WriteFile{ 37 | { 38 | FileName: "zbhome_sensor.hpp", 39 | TemplateName: "zbhome_sensor.hpp", 40 | }, 41 | { 42 | FileName: "zbhome_sensor.cpp", 43 | TemplateName: "zbhome_sensor.cpp", 44 | }, 45 | } 46 | } 47 | 48 | func (Sensor) ZephyrModules() []string { 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /templates/extenders/uart.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/ffenix113/zigbee_home/types" 8 | "github.com/ffenix113/zigbee_home/types/appconfig" 9 | "github.com/ffenix113/zigbee_home/types/devicetree" 10 | "github.com/ffenix113/zigbee_home/types/generator" 11 | ) 12 | 13 | var _ appconfig.Provider = UART{} 14 | var _ devicetree.Applier = UART{} 15 | 16 | type UARTInstance struct { 17 | // ID is a actual label of pre-defined UART peripheral. 18 | // For example most SoCs would have IDs something like uart0, uart1, ... 19 | ID string 20 | TX, RX types.Pin 21 | Speed int 22 | } 23 | 24 | type UART struct { 25 | generator.SimpleExtender 26 | 27 | Instances []UARTInstance 28 | } 29 | 30 | func NewUART(instances ...UARTInstance) UART { 31 | for i, instance := range instances { 32 | if len(instance.ID) != 5 || !strings.HasPrefix(instance.ID, "uart") || (instance.ID[4] < '0' || instance.ID[4] > '9') { 33 | panic(fmt.Sprintf("uart instance %d(%q) must have `id` format of 'uart[0-9]'", i, instance.ID)) 34 | } 35 | } 36 | 37 | return UART{ 38 | Instances: instances, 39 | } 40 | } 41 | 42 | func (i UART) AppConfig() []appconfig.ConfigValue { 43 | return []appconfig.ConfigValue{ 44 | appconfig.CONFIG_UART_CONSOLE.Required(appconfig.Yes), 45 | appconfig.CONFIG_UART_LINE_CTRL.Required(appconfig.Yes), 46 | } 47 | } 48 | 49 | func (i UART) ApplyOverlay(dt *devicetree.DeviceTree) error { 50 | pinctrl := dt.FindSpecificNode(devicetree.SearchByLabel(devicetree.NodeLabelPinctrl)) 51 | 52 | for _, instance := range i.Instances { 53 | if instance.Speed <= 0 { 54 | instance.Speed = 115200 55 | } 56 | 57 | // Pinctrl 58 | 59 | // Add pin definitions only if we have some. 60 | // Otherwise just enable the UART instance. 61 | if instance.RX.PinsDefined() && instance.TX.PinsDefined() { 62 | pinctrl.AddNodes(buildPinctrlUART(instance.ID, instance)...) 63 | } 64 | 65 | // Root item 66 | // Define, even if corrently defined on base board overlay, 67 | // just to be sure. 68 | dt.AddNodes(&devicetree.Node{ 69 | Label: instance.ID, 70 | Upsert: true, 71 | Properties: []devicetree.Property{ 72 | devicetree.NewProperty(devicetree.PropertyNameCompatible, devicetree.FromValue("nordic,nrf-uart")), 73 | devicetree.NewProperty("current-speed", devicetree.FromValue(instance.Speed)), 74 | devicetree.PropertyStatusEnable, 75 | devicetree.NewProperty("pinctrl-0", devicetree.Angled(devicetree.Label(fmt.Sprintf("%s_default", instance.ID)))), 76 | devicetree.NewProperty("pinctrl-1", devicetree.Angled(devicetree.Label(fmt.Sprintf("%s_sleep", instance.ID)))), 77 | devicetree.NewProperty("pinctrl-names", devicetree.Array(devicetree.Quoted("default"), devicetree.Quoted("sleep"))), 78 | }, 79 | }) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func buildPinctrlUART(id string, i UARTInstance) []*devicetree.Node { 86 | return []*devicetree.Node{ 87 | { 88 | Name: id + "_default", 89 | Label: id + "_default", 90 | SubNodes: []*devicetree.Node{buildUARTNode(i, false)}, 91 | }, 92 | { 93 | Name: id + "_sleep", 94 | Label: id + "_sleep", 95 | SubNodes: []*devicetree.Node{buildUARTNode(i, true)}, 96 | }, 97 | } 98 | } 99 | 100 | func buildUARTNode(i UARTInstance, lowPowerEnable bool) *devicetree.Node { 101 | group1 := &devicetree.Node{ 102 | Name: "group1", 103 | Properties: []devicetree.Property{ 104 | devicetree.NewProperty("psels", 105 | devicetree.Array( 106 | devicetree.NrfPSel("UART_TX", i.TX.Port.Value(), i.TX.Pin.Value()), 107 | devicetree.NrfPSel("UART_RX", i.RX.Port.Value(), i.RX.Pin.Value()), 108 | ), 109 | ), 110 | }, 111 | } 112 | 113 | if lowPowerEnable { 114 | group1.Properties = append(group1.Properties, devicetree.NewProperty("low-power-enable", nil)) 115 | } 116 | 117 | return group1 118 | } 119 | -------------------------------------------------------------------------------- /templates/extenders/usb.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/types/appconfig" 5 | "github.com/ffenix113/zigbee_home/types/devicetree" 6 | "github.com/ffenix113/zigbee_home/types/generator" 7 | ) 8 | 9 | var _ generator.Extender = UART{} 10 | var _ devicetree.Applier = UART{} 11 | 12 | // NewUSBUART will add necessary configuration to enable 13 | // USB UART output. 14 | // 15 | // https://docs.zephyrproject.org/latest/connectivity/usb/device/usb_device.html#console-over-cdc-acm-uart 16 | func NewUSBUART() generator.Extender { 17 | return generator.SimpleExtender{ 18 | IncludeHeaders: []string{ 19 | "zephyr/usb/usb_device.h", 20 | "zephyr/usb/usbd.h", 21 | }, 22 | Config: []appconfig.ConfigValue{ 23 | appconfig.CONFIG_USB_DEVICE_STACK.Required(appconfig.Yes), 24 | appconfig.CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT.Required(appconfig.No), 25 | }, 26 | OverlayFn: applyOverlay, 27 | } 28 | } 29 | 30 | func applyOverlay(dt *devicetree.DeviceTree) error { 31 | dt.AddNodes(&devicetree.Node{ 32 | Label: "zephyr_udc0", 33 | Upsert: true, 34 | SubNodes: []*devicetree.Node{ 35 | { 36 | Name: "cdc_acm_uart0", 37 | Label: "cdc_acm_uart0", 38 | Properties: []devicetree.Property{ 39 | devicetree.NewProperty(devicetree.PropertyNameCompatible, devicetree.Quoted("zephyr,cdc-acm-uart")), 40 | }, 41 | }, 42 | }, 43 | }) 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /templates/src/CMakeLists.txt.tpl: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 Nordic Semiconductor ASA 3 | # 4 | # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 5 | # 6 | 7 | list(APPEND ZEPHYR_EXTRA_MODULES 8 | {{ range .Extenders }}{{ with .ZephyrModules }}{{range . -}} 9 | ${CMAKE_CURRENT_SOURCE_DIR}/modules/{{.}} 10 | {{- end }}{{ end }}{{ end }} 11 | ) 12 | 13 | cmake_minimum_required(VERSION 3.20.0) 14 | 15 | ################################################################################ 16 | 17 | # The application uses the configuration/ scheme for configuration files. 18 | set(APPLICATION_CONFIG_DIR "${CMAKE_CURRENT_SOURCE_DIR}") 19 | set(BOARD_ROOT "${CMAKE_CURRENT_LIST_DIR}") 20 | 21 | find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) 22 | project(zigbee_common) 23 | 24 | ################################################################################ 25 | 26 | # NORDIC SDK APP START 27 | FILE(GLOB app_sources_cpp src/*.cpp src/**/*.cpp) 28 | FILE(GLOB app_sources src/*.c src/**/*.c) 29 | target_sources(app 30 | PRIVATE ${app_sources_cpp} 31 | PRIVATE ${app_sources} 32 | ) 33 | 34 | # NORDIC SDK APP END 35 | -------------------------------------------------------------------------------- /templates/src/Kconfig.tpl: -------------------------------------------------------------------------------- 1 | config ZBHOME_DEBUG_ENABLE 2 | bool "Debug configuration" 3 | default n 4 | help 5 | Enable debug configuration for the firmware 6 | 7 | config ZBHOME_DEBUG_LEDS 8 | bool "Enable debug LEDs" 9 | default y 10 | depends on ZBHOME_DEBUG_ENABLE 11 | help 12 | Enable LEDs to signify some debug state(on/off, joined Zigbee network). 13 | 14 | config ZBHOME_DEBUG_CONSOLE 15 | string "Output console for logs" 16 | default "" 17 | help 18 | Specifies which backend to use for logging 19 | 20 | source "Kconfig.zephyr" 21 | 22 | module = ZIGBEE_DEVICE 23 | module-str = Zigbee device -------------------------------------------------------------------------------- /templates/src/extenders/adc.c.tpl: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | LOG_MODULE_DECLARE(app, LOG_LEVEL_INF); 6 | 7 | int zigbee_home_read_adc_mv(const struct adc_dt_spec *spec, int32_t *valp) { 8 | int err; 9 | uint16_t buf; 10 | struct adc_sequence sequence = { 11 | .buffer = &buf, 12 | /* buffer size in bytes, not number of samples */ 13 | .buffer_size = sizeof(buf), 14 | }; 15 | 16 | (void)adc_sequence_init_dt(spec, &sequence); 17 | err = adc_read(spec->dev, &sequence); 18 | if (err < 0) { 19 | LOG_DBG("ADC %s@%d: Could not read (%d)\n", spec->dev->name, spec->channel_id, err); 20 | return err; 21 | } 22 | 23 | /* 24 | * If using differential mode, the 16 bit value 25 | * in the ADC sample buffer should be a signed 2's 26 | * complement value. 27 | */ 28 | int32_t val_mv; 29 | if (spec->channel_cfg.differential) { 30 | val_mv = (int32_t)((int16_t)buf); 31 | } else { 32 | val_mv = (int32_t)buf; 33 | } 34 | 35 | LOG_DBG("ADC %s@%d raw value: %d", spec->dev->name, spec->channel_id, val_mv); 36 | err = adc_raw_to_millivolts_dt(spec, &val_mv); 37 | /* conversion to mV may not be supported, skip if not */ 38 | if (err < 0) { 39 | LOG_DBG(" (value in mV not available)"); 40 | return err; 41 | } 42 | 43 | LOG_DBG("ADC %s@%d mv value: %d", spec->dev->name, spec->channel_id, val_mv); 44 | 45 | *valp = val_mv; 46 | return 0; 47 | } -------------------------------------------------------------------------------- /templates/src/extenders/adc.h.tpl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | int zigbee_home_read_adc_mv(const struct adc_dt_spec *spec, int32_t *valp); -------------------------------------------------------------------------------- /templates/src/extenders/peripherals/adc.tpl: -------------------------------------------------------------------------------- 1 | {{define "top_level"}} 2 | /* Data of ADC io-channels specified in devicetree. */ 3 | {{ range $i, $instance := .Extender.Instances }} 4 | static const struct adc_dt_spec adc_channel_{{$instance.Name}} = ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), {{$i}}); 5 | {{end}} 6 | {{end}} 7 | 8 | {{ define "loop"}} {{end}} 9 | 10 | 11 | {{ define "main"}} 12 | int err; 13 | {{ range .Extender.Instances }} 14 | err = adc_channel_setup_dt(&adc_channel_{{.Name}}); 15 | if (err < 0) { 16 | LOG_ERR("Could not setup channel '{{.Name}}' (%d)\n", err); 17 | return 0; 18 | } 19 | {{end}} 20 | {{end}} -------------------------------------------------------------------------------- /templates/src/extenders/peripherals/buttons.tpl: -------------------------------------------------------------------------------- 1 | {{/* The templates are non-empty to force their usage. */}} 2 | {{ define "top_level" }} 3 | {{- range .Extender.Instances }} 4 | static struct gpio_dt_spec {{.ID}} = GPIO_DT_SPEC_GET(DT_NODELABEL({{.ID}}), gpios); 5 | static const uint32_t {{ toButtonName .ID}} = {{ toButtonIdx .ID }}; 6 | static const uint32_t {{ toButtonBitName .ID}} = {{ toButtonBit .ID }}; 7 | {{- end }} 8 | {{end}} 9 | 10 | {{ define "loop"}} {{end}} 11 | 12 | {{ define "main"}} 13 | // Buttons will be configured by DK library. 14 | {{end}} -------------------------------------------------------------------------------- /templates/src/extenders/peripherals/leds.tpl: -------------------------------------------------------------------------------- 1 | {{/* The templates are non-empty to force their usage. */}} 2 | {{ define "top_level" }} 3 | {{- range .Extender.Instances }} 4 | static struct gpio_dt_spec {{.ID}} = GPIO_DT_SPEC_GET(DT_NODELABEL({{.ID}}), gpios); 5 | {{- end }} 6 | {{end}} 7 | 8 | {{ define "loop"}} 9 | 10 | {{end}} 11 | 12 | {{ define "main"}} 13 | int ret; 14 | {{- range .Extender.Instances}} 15 | ret = gpio_pin_configure_dt(&{{.ID}}, GPIO_OUTPUT); 16 | if (ret != 0) { 17 | LOG_ERR("Error %d: failed to configure LED device %s pin %d\n", 18 | ret, {{.ID}}.port->name, {{.ID}}.pin); 19 | return ret; 20 | } 21 | {{- end}} 22 | {{end}} -------------------------------------------------------------------------------- /templates/src/extenders/pm_static.yml.tpl: -------------------------------------------------------------------------------- 1 | {{- range .AdditionalContext }} 2 | {{ .Name }}: 3 | address: {{formatHex .Address}} 4 | end_address: {{ formatHex .EndAddress }} 5 | region: {{ if .Region }}{{.Region}}{{else}}flash_primary{{end}} 6 | size: {{ formatHex .Size }} 7 | {{- end}} 8 | -------------------------------------------------------------------------------- /templates/src/extenders/sensors/common.tpl: -------------------------------------------------------------------------------- 1 | {{/* The templates are non-empty to force their usage. */}} 2 | {{ define "top_level" }} {{end}} 3 | {{ define "button_changed"}} {{/* button_status = has_button_changed(&{{.Sensor.Pin.Label}}, button_state, has_changed); */}}{{end}} 4 | {{ define "loop"}} {{end}} 5 | {{ define "main"}} {{end}} -------------------------------------------------------------------------------- /templates/src/extenders/sensors/device_temperature.tpl: -------------------------------------------------------------------------------- 1 | {{ define "main"}} 2 | nrfx_temp_config_t config = NRFX_TEMP_DEFAULT_CONFIG; 3 | int err = nrfx_temp_init(&config, NULL); 4 | if (err != NRFX_SUCCESS) 5 | { 6 | LOG_ERR("error initing device temp sensor"); 7 | return err; 8 | } 9 | {{ end}} 10 | 11 | {{ define "top_level" }} 12 | {{/* No need to define a device, as it is handled by NRFX lib */}} 13 | {{- end}} 14 | 15 | {{ define "loop"}} 16 | int res = nrfx_temp_measure(); 17 | if (res != NRFX_SUCCESS) { 18 | LOG_ERR("measure internal temperature failed"); 19 | } else { 20 | int32_t device_temp = nrfx_temp_result_get(); 21 | int32_t real_device_temp = nrfx_temp_calculate(device_temp); 22 | 23 | /* Set ZCL attribute */ 24 | {{ $cluster := (index .Sensor.Clusters 0) }} 25 | zb_zcl_status_t status = zb_zcl_set_attr_val({{.Endpoint}}, 26 | {{ $cluster.ID }}, 27 | ZB_ZCL_CLUSTER_SERVER_ROLE, 28 | 0x000, // CurrentTemperature attr. Hardcoding for now. Need better approach. 29 | (zb_uint8_t *)&real_device_temp, 30 | ZB_FALSE); 31 | if (status) { 32 | LOG_ERR("Failed to set ZCL attribute for internal temp: %d", status); 33 | } 34 | } 35 | {{ end}} -------------------------------------------------------------------------------- /templates/src/extenders/sensors/ias_zone.tpl: -------------------------------------------------------------------------------- 1 | {{ define "top_level" }} 2 | // For some reason ZB_SCHEDULE_CALLBACK is not defined with current setup, 3 | // and I can't find a necessary include/config to enable it. 4 | // So for now - re-define the callback as another callback. 5 | #ifndef ZBHOME_IAS_ZONE_TOP_LEVEL 6 | #define ZBHOME_IAS_ZONE_TOP_LEVEL 7 | 8 | #define ZB_SCHEDULE_CALLBACK ZB_SCHEDULE_APP_CALLBACK 9 | void update_zone_status(zb_bufid_t bufid, zb_uint16_t cb_data) { 10 | // TODO: Probably this function needs to free bufid somewhere, 11 | // but I am not sure if it is actually the case. 12 | // Would need to double-check. 13 | 14 | // Decode values from the argument. 15 | bool status = cb_data & 1; 16 | zb_uint8_t endpoint = cb_data >> 1; 17 | 18 | switch (status) { 19 | case true: 20 | ZB_ZCL_IAS_ZONE_SET_BITS(bufid, endpoint, ZB_ZCL_IAS_ZONE_ZONE_STATUS_ALARM1); 21 | break; 22 | case false: 23 | ZB_ZCL_IAS_ZONE_CLEAR_BITS(bufid, endpoint, ZB_ZCL_IAS_ZONE_ZONE_STATUS_ALARM1); 24 | break; 25 | } 26 | } 27 | #endif 28 | {{ end }} 29 | 30 | {{ define "button_changed"}} 31 | bool button_pressed = button_state & {{ toButtonBitName .Sensor.Button}}; 32 | bool button_changed = has_changed & {{ toButtonBitName .Sensor.Button}}; 33 | if (button_changed) { 34 | // Pack data. 35 | // Endpoint can be >127, so we can't pack it and state into single uint8. 36 | zb_uint16_t data = {{.Endpoint}} << 1 | button_pressed; 37 | 38 | /* Allocate output buffer and send on/off command. */ 39 | zb_ret_t zb_err_code = zb_buf_get_out_delayed_ext( 40 | update_zone_status, data, 0); 41 | ZB_ERROR_CHECK(zb_err_code); 42 | } 43 | {{end}} 44 | 45 | {{ define "loop" }} {{end}} 46 | {{ define "main"}} {{ end}} -------------------------------------------------------------------------------- /templates/src/extenders/sensors/on_off.tpl: -------------------------------------------------------------------------------- 1 | {{ define "top_level" }} 2 | static const struct gpio_dt_spec {{.Sensor.Pin.Label}} = GPIO_DT_SPEC_GET(DT_NODELABEL({{.Sensor.Pin.Label}}), gpios); 3 | {{ end }} 4 | 5 | {{ define "loop" }} {{end}} 6 | 7 | {{ define "main"}} 8 | if (!gpio_is_ready_dt(&{{.Sensor.Pin.Label}})) { 9 | LOG_ERR("Pin {{.Sensor.Pin.Label}} is not ready"); 10 | return -1; 11 | } 12 | 13 | int err = gpio_pin_configure_dt(&{{.Sensor.Pin.Label}}, GPIO_OUTPUT); 14 | if (err != 0) { 15 | LOG_ERR("Cannot configure pin {{.Sensor.Pin.Label}}"); 16 | return err; 17 | } 18 | {{ end}} -------------------------------------------------------------------------------- /templates/src/extenders/sensors/power_config.tpl: -------------------------------------------------------------------------------- 1 | {{/* The templates are non-empty to force their usage. */}} 2 | {{ define "top_level" }} {{end}} 3 | {{ define "button_changed"}} {{/* button_status = has_button_changed(&{{.Sensor.Pin.Label}}, button_state, has_changed); */}}{{end}} 4 | {{ define "loop"}} 5 | 6 | int32_t batt_mv; 7 | int err = zigbee_home_read_adc_mv(&adc_channel_{{.Sensor.ADCPin.Name}}, &batt_mv); 8 | if (err) { 9 | LOG_ERR("Failed to read ADC value from ADC channel {{.Sensor.ADCPin.Name}}"); 10 | } 11 | {{ $cluster := (index .Sensor.Clusters 0) }} 12 | zb_uint8_t batt_mv_divided = batt_mv / 100; 13 | zb_zcl_status_t status = zb_zcl_set_attr_val({{.Endpoint}}, 14 | {{ $cluster.ID }}, 15 | ZB_ZCL_CLUSTER_SERVER_ROLE, 16 | ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_VOLTAGE_ID, 17 | &(batt_mv_divided), 18 | ZB_FALSE); 19 | if (status) { 20 | LOG_ERR("Failed to set ZCL attribute for battery voltage sensor: %d", status); 21 | } 22 | {/* Endpoint subtracts 1 because seems that there is a bug 23 | in how endpoints are calculated for attribute structs. 24 | */} 25 | zb_uint16_t rated_voltage = dev_ctx.{{$cluster.CVarName}}_{{sum .Endpoint -1}}_attrs.rated_voltage * 100; 26 | zb_uint16_t min_threshold = dev_ctx.{{$cluster.CVarName}}_{{sum .Endpoint -1}}_attrs.voltage_min_threshold * 100; 27 | 28 | zb_uint8_t percentage; 29 | if (batt_mv >= rated_voltage) { 30 | percentage = 200; 31 | } else if (batt_mv <= min_threshold) { 32 | percentage = 0; 33 | } else { 34 | percentage = (((batt_mv-min_threshold)*200) / (rated_voltage - min_threshold)); 35 | } 36 | 37 | // Have only 10% increments 38 | percentage = ((percentage + 10) / 20) * 20; 39 | 40 | LOG_DBG("ADC battery channel {{.Sensor.ADCPin.Name}}: rated: %d, threshold: %d, current mv: %d, percent(x2): %d", rated_voltage, min_threshold, batt_mv, percentage); 41 | status = zb_zcl_set_attr_val({{.Endpoint}}, 42 | {{ $cluster.ID }}, 43 | ZB_ZCL_CLUSTER_SERVER_ROLE, 44 | ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_PERCENTAGE_REMAINING_ID, 45 | &percentage, 46 | ZB_FALSE); 47 | if (status) { 48 | LOG_ERR("Failed to set ZCL attribute for battery percentage sensor: %d", status); 49 | } 50 | {{end}} 51 | {{ define "main"}} {{end}} -------------------------------------------------------------------------------- /templates/src/extenders/sensors/soil_moisture_adc.tpl: -------------------------------------------------------------------------------- 1 | {{/* The templates are non-empty to force their usage. */}} 2 | {{ define "top_level" }} {{end}} 3 | {{ define "button_changed"}} {{/* button_status = has_button_changed(&{{.Sensor.Pin.Label}}, button_state, has_changed); */}}{{end}} 4 | {{ define "loop"}} 5 | int32_t sensor_mv; 6 | int err = zigbee_home_read_adc_mv(&adc_channel_{{.Sensor.ADCPin.Name}}, &sensor_mv); 7 | if (err) { 8 | LOG_ERR("Failed to read ADC value from ADC channel {{.Sensor.ADCPin.Name}}"); 9 | } 10 | 11 | // This values are from sensor configuration. 12 | // Max value is actually minimal moisture, 13 | // and min value is maximal moisture. 14 | const zb_uint16_t min_mv_val = {{.Sensor.MaxMoistureMv}}; 15 | const zb_uint16_t max_mv_val = {{.Sensor.MinMoistureMv}}; 16 | 17 | // Assume that all ADC moisture sensors will 18 | // return smaller values with higher soil moisture content. 19 | zb_uint16_t percentage; 20 | if (sensor_mv >= max_mv_val) { 21 | percentage = 0; 22 | } else if (sensor_mv <= min_mv_val) { 23 | percentage = 100; 24 | } else { 25 | percentage = 100 - (((sensor_mv-min_mv_val)*100) / (max_mv_val - min_mv_val)); 26 | } 27 | 28 | // Have only 5% increments to not be very noisy, 29 | // this could change in the future though. 30 | percentage = ((percentage + 5) / 10) * 10; 31 | 32 | LOG_DBG("ADC soil moisture channel {{.Sensor.ADCPin.Name}}: min_mv: %d, max_mv: %d, current mv: %d, percent: %d", min_mv_val, max_mv_val, sensor_mv, percentage); 33 | 34 | percentage *= 100; 35 | {{ $cluster := (index .Sensor.Clusters 0) }} 36 | int status = zb_zcl_set_attr_val({{.Endpoint}}, 37 | {{ $cluster.ID }}, 38 | ZB_ZCL_CLUSTER_SERVER_ROLE, 39 | ZB_ZCL_ATTR_REL_HUMIDITY_MEASUREMENT_VALUE_ID, 40 | (zb_uint8_t*)&percentage, 41 | ZB_FALSE); 42 | if (status) { 43 | LOG_ERR("Failed to set ZCL attribute for soil moisture sensor: %d", status); 44 | } 45 | {{end}} 46 | {{ define "main"}} {{end}} -------------------------------------------------------------------------------- /templates/src/extenders/zbhome_sensor.cpp.tpl: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "zbhome_sensor.hpp" 5 | #include "clusters.hpp" 6 | 7 | LOG_MODULE_DECLARE(app, LOG_LEVEL_INF); 8 | 9 | #ifdef ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT 10 | GENERATE_ATTR_VAL_SETTER(temperature, TEMP_MEASUREMENT) 11 | GENERATE_ATTR_SENSOR_VALUE_SETTER(temperature, float, int16_t, ZCL_TEMPERATURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER) 12 | GENERATE_SENSOR_FULL_FOR_ATTR(temperature, SENSOR_CHAN_AMBIENT_TEMP) 13 | #endif 14 | 15 | #ifdef ZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT 16 | GENERATE_ATTR_VAL_SETTER(humidity, REL_HUMIDITY_MEASUREMENT) 17 | GENERATE_ATTR_SENSOR_VALUE_SETTER(humidity, float, int16_t, ZCL_HUMIDITY_MEASUREMENT_MEASURED_VALUE_MULTIPLIER) 18 | GENERATE_SENSOR_FULL_FOR_ATTR(humidity, SENSOR_CHAN_HUMIDITY) 19 | #endif 20 | 21 | #ifdef ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT 22 | GENERATE_ATTR_VAL_SETTER(pressure, PRESSURE_MEASUREMENT) 23 | GENERATE_ATTR_SENSOR_VALUE_SETTER(pressure, float, int16_t, ZCL_PRESSURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER) 24 | GENERATE_SENSOR_FULL_FOR_ATTR(pressure, SENSOR_CHAN_PRESS) 25 | #endif 26 | 27 | #ifdef ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE 28 | GENERATE_ATTR_VAL_SETTER(carbon_dioxide, CARBON_DIOXIDE) 29 | GENERATE_ATTR_SENSOR_VALUE_SETTER(carbon_dioxide, float, float, ZCL_CARBON_DIOXIDE_MEASURED_VALUE_MULTIPLIER) 30 | GENERATE_SENSOR_FULL_FOR_ATTR(carbon_dioxide, SENSOR_CHAN_CO2) 31 | #endif -------------------------------------------------------------------------------- /templates/src/extenders/zbhome_sensor.hpp.tpl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define GENERATE_ATTR_VAL_SETTER(cluster_name, cluster) \ 4 | zb_zcl_status_t zbhome_set_attr_val_for_##cluster_name(int endpoint, zb_uint8_t * data_ptr) { \ 5 | return zb_zcl_set_attr_val( \ 6 | endpoint, \ 7 | ZB_ZCL_CLUSTER_ID_##cluster, \ 8 | ZB_ZCL_CLUSTER_SERVER_ROLE, \ 9 | ZB_ZCL_ATTR_##cluster##_VALUE_ID, \ 10 | data_ptr, \ 11 | ZB_FALSE); \ 12 | } 13 | 14 | #define GENERATE_ATTR_SENSOR_VALUE_SETTER(cluster_name, measured_type, attribute_type, multiplier) \ 15 | zb_zcl_status_t zbhome_set_attr_sensor_value_for_##cluster_name(int endpoint, struct sensor_value * value) { \ 16 | measured_type measured_value = sensor_value_to_##measured_type(value); \ 17 | attribute_type attr_value = (measured_value * multiplier); \ 18 | return zbhome_set_attr_val_for_##cluster_name(endpoint, (zb_uint8_t*)&attr_value); \ 19 | } 20 | 21 | #define NAME_GENERATE_SENSOR_FULL_FOR_ATTR(cluster_name) int zbhome_sensor_fetch_and_update_##cluster_name(const struct device * sensor, int endpoint) 22 | #define GENERATE_SENSOR_FULL_FOR_ATTR(cluster_name, sensor_channel) \ 23 | int zbhome_sensor_fetch_and_update_##cluster_name(const struct device * sensor, int endpoint) { \ 24 | struct sensor_value value; \ 25 | int err = sensor_channel_get(sensor, sensor_channel, &value); \ 26 | if (err) { \ 27 | LOG_ERR("Failed to get sensor %s channel %s: %d", sensor->name, #cluster_name, err); \ 28 | return err; \ 29 | } \ 30 | LOG_INF("Sensor raw %s/%s: %6d.%06d", sensor->name, #cluster_name, value.val1, value.val2); \ 31 | err = (int)zbhome_set_attr_sensor_value_for_##cluster_name(endpoint, &value); \ 32 | if (err) { \ 33 | LOG_ERR("Failed to set ZCL attribute for sensor %s, cluster %s: %d", sensor->name, #cluster_name, err); \ 34 | } \ 35 | return err; \ 36 | } 37 | 38 | #ifdef ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT 39 | NAME_GENERATE_SENSOR_FULL_FOR_ATTR(temperature); 40 | #endif 41 | 42 | #ifdef ZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT 43 | NAME_GENERATE_SENSOR_FULL_FOR_ATTR(humidity); 44 | #endif 45 | 46 | #ifdef ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT 47 | NAME_GENERATE_SENSOR_FULL_FOR_ATTR(pressure); 48 | #endif 49 | 50 | #ifdef ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE 51 | NAME_GENERATE_SENSOR_FULL_FOR_ATTR(carbon_dioxide); 52 | #endif 53 | -------------------------------------------------------------------------------- /templates/src/modules/scd4x/dts/bindings/sensor/sensirion,scd4x.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2023 Jan Gnip 3 | # Copyright (c) 2022, Stephen Oliver 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | 7 | description: Sensirion SCD4x co2, humidity, and temperature sensor 8 | 9 | compatible: "sensirion,scd4x" 10 | 11 | include: i2c-device.yaml 12 | 13 | properties: 14 | model: 15 | type: string 16 | required: true 17 | description: | 18 | The sensor model in use 19 | enum: 20 | - "scd40" 21 | - "scd41" 22 | measure-mode: 23 | type: string 24 | required: true 25 | description: | 26 | Normal: Background measurement produces a new sample every 5 seconds 27 | 28 | Low power mode (SCD41 only): Similar to normal mode, sensor uses 3mA 29 | rather than 13-18mA. Produces a new sample every 30 seconds. 30 | 31 | Single-shot mode (SCD41 only): Similar to low-power mode but samples immediately 32 | after sensor_sample_fetch() is called. See option single-shot-power-down to put 33 | sensor to sleep between samples. 34 | enum: 35 | - "normal" 36 | - "low-power" 37 | - "single-shot" 38 | single-shot-power-down: 39 | type: boolean 40 | required: false 41 | description: | 42 | Will send the power_down command to the sensor after a measurement, and wake it up again 43 | before the next measurement. Only works on SCD41 model. 44 | auto-calibration: 45 | type: boolean 46 | required: false 47 | description: | 48 | Recalibrates the sensor's 400ppm reference level to match lowest CO2 level seen in 49 | past 7 days (assumed to be outdoor air). Disable if the sensor is never exposed to 50 | outdoor air to avoid severe accuracy drift. 51 | temperature-offset: 52 | type: int 53 | required: true 54 | default: 4 55 | description: | 56 | Degrees centigrade. Sensor subtracts this from temperature before calculating 57 | humidity. Does not affect CO2 sample. Use when sensor is inside a device that 58 | is hotter than ambient temperature. 59 | altitude: 60 | type: int 61 | required: false 62 | default: 0 63 | description: | 64 | Meters above sea level, improves CO2 accuracy. Sensor uses it to calculate 65 | ambient pressure. 66 | 67 | The scd4x_set_ambient_pressure() function in the driver can be called at any 68 | time to update the sensor calculations, which will override the altitude setting. 69 | -------------------------------------------------------------------------------- /templates/src/modules/scd4x/zephyr/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | if(CONFIG_SCD4X) 4 | zephyr_include_directories(.) 5 | 6 | zephyr_library() 7 | zephyr_library_sources(scd4x.c) 8 | endif() -------------------------------------------------------------------------------- /templates/src/modules/scd4x/zephyr/Kconfig: -------------------------------------------------------------------------------- 1 | # SCD4x co2, temperature, and humidity sensor configuration options 2 | 3 | # Copyright (c) 2022 Stephen Oliver 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | config SCD4X 7 | bool "SCD4x CO2/Temperature/Humidity Sensor" 8 | depends on I2C 9 | depends on DT_HAS_SENSIRION_SCD4X_ENABLED 10 | help 11 | Enable driver for SCD4x co2, temperature, and humidity sensors. 12 | -------------------------------------------------------------------------------- /templates/src/modules/scd4x/zephyr/module.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2019 Nordic Semiconductor 3 | 4 | name: sensirion,scd4 5 | 6 | build: 7 | cmake: zephyr 8 | kconfig: zephyr/Kconfig 9 | settings: 10 | dts_root: . 11 | -------------------------------------------------------------------------------- /templates/src/modules/scd4x/zephyr/scd4x.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Jan Gnip 3 | * Copyright (c) 2022, Stephen Oliver 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | /** 9 | * @file 10 | * @brief API for Sensirion SCD4X CO2/T/RH sensors 11 | * 12 | * Only provides access to the sensor's pressure level setting, used for increasing CO2 accuracy. 13 | */ 14 | 15 | #ifndef ZEPHYR_DRIVERS_SENSOR_SCD4X_H_ 16 | #define ZEPHYR_DRIVERS_SENSOR_SCD4X_H_ 17 | 18 | #ifdef __cplusplus 19 | extern "C" { 20 | #endif 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | #define SCD4X_MAX_AMBIENT_PRESSURE UINT16_MAX 27 | 28 | #define SCD4X_POWER_DOWN_WAIT_MS 1 29 | #define SCD4X_WAKE_UP_WAIT_MS 20 30 | #define SCD4X_REINIT_WAIT_MS 20 31 | #define SCD4X_PERFORM_SELF_TEST_WAIT_MS 10000 32 | #define SCD4X_PERFORM_FACTORY_RESET_WAIT_MS 1200 33 | #define SCD4X_STOP_PERIODIC_MEASUREMENT_WAIT_MS 500 34 | #define SCD4X_READ_MEASUREMENT_WAIT_MS 1 35 | #define SCD4X_SET_TEMPERATURE_OFFSET_WAIT_MS 1 36 | #define SCD4X_GET_TEMPERATURE_OFFSET_WAIT_MS 1 37 | #define SCD4X_SET_SENSOR_ALTITUDE_WAIT_MS 1 38 | #define SCD4X_GET_SENSOR_ALTITUDE_WAIT_MS 1 39 | #define SCD4X_SET_AMBIENT_PRESSURE_WAIT_MS 1 40 | #define SCD4X_SET_AUTOMATIC_CALIBRATION_WAIT_MS 1 41 | #define SCD4X_MEASURE_SINGLE_SHOT_WAIT_MS 5000 42 | #define SCD4X_MEASURE_SINGLE_SHOT_RHT_ONLY_WAIT_MS 50 43 | 44 | /* 45 | * Used to mask SCD4X_CMD_GET_DATA_READY_STATUS response value. 46 | * The sensor datasheet does not document the meaning of each bit, nor does it state that any 47 | * particular bit will be set to 1 when data is ready, it only guarantees that the device is 48 | * NOT ready if these bits are all 0, and that any other value means data is ready. 49 | */ 50 | #define SCD4X_MEASURE_READY(x) (((x) & 0x07FF) != 0) 51 | 52 | /* 53 | * CRC parameters from SCD4X datasheet version 1.2, section 3.11 54 | */ 55 | #define SCD4X_CRC_POLY 0x31 56 | #define SCD4X_CRC_INIT 0xFF 57 | 58 | enum scd4x_command { 59 | SCD4X_CMD_POWER_DOWN = 0x36E0, 60 | SCD4X_CMD_WAKE_UP = 0x36F6, 61 | SCD4X_CMD_REINIT = 0x3646, 62 | SCD4X_CMD_START_PERIODIC_MEASUREMENT = 0x21B1, 63 | SCD4X_CMD_STOP_PERIODIC_MEASUREMENT = 0x3F86, 64 | SCD4X_CMD_START_LOW_POWER_PERIODIC_MEASUREMENT = 0x21AC, 65 | SCD4X_CMD_GET_DATA_READY_STATUS = 0xE4B8, 66 | SCD4X_CMD_READ_MEASUREMENT = 0xEC05, 67 | SCD4X_CMD_PERSIST_SETTINGS = 0x3615, 68 | SCD4X_CMD_GET_SERIAL_NUMBER = 0x3682, 69 | SCD4X_CMD_PERFORM_SELF_TEST = 0x3639, 70 | SCD4X_CMD_PERFORM_FACTORY_RESET = 0x3632, 71 | SCD4X_CMD_SET_TEMPERATURE_OFFSET = 0x241D, 72 | SCD4X_CMD_GET_TEMPERATURE_OFFSET = 0x2318, 73 | SCD4X_CMD_SET_SENSOR_ALTITUDE = 0x2427, 74 | SCD4X_CMD_GET_SENSOR_ALTITUDE = 0x2322, 75 | SCD4X_CMD_SET_AMBIENT_PRESSURE = 0xE000, 76 | SCD4X_CMD_PERFORM_FORCED_RECALIBRATION = 0x362F, 77 | SCD4X_CMD_SET_AUTOMATIC_SELF_CALIBRATION_ENABLED = 0x2416, 78 | SCD4X_CMD_GET_AUTOMATIC_SELF_CALIBRATION_ENABLED = 0x2313, 79 | SCD4X_CMD_MEASURE_SINGLE_SHOT = 0x219D, 80 | SCD4X_CMD_MEASURE_SINGLE_SHOT_RHT_ONLY = 0x2196 81 | }; 82 | 83 | enum scd4x_model { 84 | SCD4X_MODEL_SCD40, 85 | SCD4X_MODEL_SCD41, 86 | }; 87 | 88 | enum scd4x_measure_mode { 89 | SCD4X_MEASURE_MODE_NORMAL, 90 | SCD4X_MEASURE_MODE_LOW_POWER, 91 | SCD4X_MEASURE_MODE_SINGLE_SHOT, 92 | }; 93 | 94 | struct scd4x_config { 95 | struct i2c_dt_spec bus; 96 | enum scd4x_model model; 97 | enum scd4x_measure_mode measure_mode; 98 | bool single_shot_power_down; 99 | bool auto_calibration; 100 | uint16_t temperature_offset; 101 | uint16_t altitude; 102 | }; 103 | 104 | struct scd4x_data { 105 | uint16_t t_sample; 106 | uint16_t rh_sample; 107 | uint16_t co2_sample; 108 | char serial_number[15]; 109 | }; 110 | 111 | 112 | /** 113 | * @brief Updates the sensor ambient pressure value used for increasing CO2 accuracy. Overrides the altitude 114 | * set in the device tree. Can be set at any time. 115 | * 116 | * @param dev Pointer to the sensor device 117 | * 118 | * @param pressure Ambient pressure, unit is Pascal 119 | * 120 | * @return 0 if successful, negative errno code if failure. 121 | */ 122 | int scd4x_set_ambient_pressure(const struct device *dev, uint16_t pressure); 123 | 124 | #ifdef __cplusplus 125 | } 126 | #endif 127 | 128 | #endif /* ZEPHYR_DRIVERS_SENSOR_SCD4X_H_ */ 129 | -------------------------------------------------------------------------------- /templates/src/zigbee/attribute_setter.h.tpl: -------------------------------------------------------------------------------- 1 | #define ATTR_SET_VAL_FOR_TYPE(attr_type) \ 2 | int attr_set_val_##attr_type##(int endpoint, attr_type value) { \ 3 | 4 | } 5 | 6 | int attr_set_val_int(int endpoint, const struct device *sensor) 7 | { 8 | int err = 0; 9 | 10 | float measured_pressure = 0.0f; 11 | int16_t pressure_attribute = 0; 12 | 13 | err = bosch_bme680_get_pressure(sensor, &measured_pressure); 14 | if (err) { 15 | LOG_ERR("Failed to get sensor pressure: %d", err); 16 | } else { 17 | /* Convert measured value to attribute value, as specified in ZCL */ 18 | pressure_attribute = (int16_t) 19 | (measured_pressure * 20 | ZCL_PRESSURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER); 21 | LOG_INF("Attribute P:%10d", pressure_attribute); 22 | 23 | /* Set ZCL attribute */ 24 | zb_zcl_status_t status = zb_zcl_set_attr_val( 25 | endpoint, 26 | ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT, 27 | ZB_ZCL_CLUSTER_SERVER_ROLE, 28 | ZB_ZCL_ATTR_PRESSURE_MEASUREMENT_VALUE_ID, 29 | (zb_uint8_t *)&pressure_attribute, 30 | ZB_FALSE); 31 | if (status) { 32 | LOG_ERR("Failed to set ZCL attribute: %d", status); 33 | err = status; 34 | } 35 | } 36 | 37 | return err; 38 | } 39 | 40 | int bosch_bme680_update_pressure(int endpoint, const struct device *sensor) 41 | { 42 | int err = 0; 43 | 44 | float measured_pressure = 0.0f; 45 | int16_t pressure_attribute = 0; 46 | 47 | err = bosch_bme680_get_pressure(sensor, &measured_pressure); 48 | if (err) { 49 | LOG_ERR("Failed to get sensor pressure: %d", err); 50 | } else { 51 | /* Convert measured value to attribute value, as specified in ZCL */ 52 | pressure_attribute = (int16_t) 53 | (measured_pressure * 54 | ZCL_PRESSURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER); 55 | LOG_INF("Attribute P:%10d", pressure_attribute); 56 | 57 | /* Set ZCL attribute */ 58 | zb_zcl_status_t status = zb_zcl_set_attr_val( 59 | endpoint, 60 | ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT, 61 | ZB_ZCL_CLUSTER_SERVER_ROLE, 62 | ZB_ZCL_ATTR_PRESSURE_MEASUREMENT_VALUE_ID, 63 | (zb_uint8_t *)&pressure_attribute, 64 | ZB_FALSE); 65 | if (status) { 66 | LOG_ERR("Failed to set ZCL attribute: %d", status); 67 | err = status; 68 | } 69 | } 70 | 71 | return err; 72 | } -------------------------------------------------------------------------------- /templates/src/zigbee/attributes.c.tpl: -------------------------------------------------------------------------------- 1 | {{ define "additional_types" }} 2 | 3 | // Types for some clusters 4 | typedef struct { 5 | zb_int16_t current_temperature; 6 | } zb_zcl_device_temperature_config_attrs_t; 7 | 8 | typedef struct { 9 | zb_int16_t measure_value; 10 | zb_int16_t min_measure_value; 11 | zb_int16_t max_measure_value; 12 | zb_uint16_t tolerance; 13 | } zb_zcl_pressure_measurement_attrs_t; 14 | 15 | typedef struct { 16 | zb_uint32_t measure_value; 17 | zb_uint32_t min_measure_value; 18 | zb_uint32_t max_measure_value; 19 | zb_uint32_t tolerance; 20 | } zb_zcl_measurement_type_single_attrs_t; 21 | 22 | typedef struct { 23 | zb_uint16_t measure_value; 24 | zb_uint16_t min_measure_value; 25 | zb_uint16_t max_measure_value; 26 | } zb_zcl_water_content_attrs_t; 27 | 28 | typedef struct { 29 | zb_int8_t zone_state; 30 | zb_int16_t zone_type; 31 | zb_int16_t zone_status; 32 | zb_uint8_t zone_id; 33 | zb_int64_t ias_cie_address; 34 | zb_int8_t cie_short_addr; 35 | zb_int16_t cie_ep; 36 | zb_uint8_t number_of_zone_sens_levels_supported; 37 | zb_uint8_t current_zone_sens_level; 38 | } zb_zcl_ias_zone_attrs_t; 39 | 40 | typedef struct { 41 | zb_uint8_t voltage; 42 | zb_uint8_t rated_voltage; 43 | zb_uint8_t alarm_mask; 44 | zb_uint8_t voltage_min_threshold; 45 | zb_uint8_t percentage_remaining; 46 | } zb_zcl_power_config_attrs_t; 47 | // 48 | 49 | // Types for used clusters. 50 | {{- range $i, $cluster := .UniqueClusters}} 51 | {{- with (maybeRender (clusterTpl $cluster.ID "attr_types") (clusterCtx $i $cluster))}} 52 | // {{ $cluster.CVarName }} 53 | {{.}} 54 | {{- end}} 55 | {{end}} 56 | // End types for used clusters. 57 | 58 | {{end}} -------------------------------------------------------------------------------- /templates/src/zigbee/basic.c.tpl: -------------------------------------------------------------------------------- 1 | {{ define "basic_attr_list" }} 2 | ZB_ZCL_DECLARE_BASIC_ATTRIB_LIST_EXT( 3 | {{.Cluster.CVarName}}_{{.Endpoint}}_attr_list, 4 | &dev_ctx.basic_attr.zcl_version, 5 | &dev_ctx.basic_attr.app_version, 6 | &dev_ctx.basic_attr.stack_version, 7 | &dev_ctx.basic_attr.hw_version, 8 | dev_ctx.basic_attr.mf_name, 9 | dev_ctx.basic_attr.model_id, 10 | dev_ctx.basic_attr.date_code, 11 | &dev_ctx.basic_attr.power_source, 12 | dev_ctx.basic_attr.location_id, 13 | &dev_ctx.basic_attr.ph_env, 14 | dev_ctx.basic_attr.sw_ver); 15 | 16 | {{ end }} -------------------------------------------------------------------------------- /templates/src/zigbee/carbon_dioxide.tpl: -------------------------------------------------------------------------------- 1 | {{ define "carbon_dioxide_defines"}} 2 | // ZCL spec 4.13.1.3 3 | #define ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE (0x040d) 4 | 5 | // ZCL spec 4.13.1.1 6 | #define ZB_ZCL_CARBON_DIOXIDE_CLUSTER_REVISION_DEFAULT ((zb_uint16_t)0x0002u) 7 | 8 | #define ZB_ZCL_ATTR_CARBON_DIOXIDE_VALUE_UNKNOWN ZB_ZCL_ATTR_PRESSURE_MEASUREMENT_VALUE_UNKNOWN 9 | 10 | #define ZCL_CARBON_DIOXIDE_MEASURED_VALUE_MULTIPLIER 0.000001 11 | 12 | /*! @brief CurrentTemperature, ZCL spec 3.4.2.2.1 */ 13 | #define ZB_ZCL_ATTR_CARBON_DIOXIDE_VALUE_ID (0x0000) 14 | #define ZB_ZCL_ATTR_CARBON_DIOXIDE_MIN_VALUE_ID (0x0001) 15 | #define ZB_ZCL_ATTR_CARBON_DIOXIDE_MAX_VALUE_ID (0x0002) 16 | #define ZB_ZCL_ATTR_CARBON_DIOXIDE_TOLERANCE_ID (0x0003) 17 | {{end}} 18 | 19 | {{ define "carbon_dioxide_attr_types"}} 20 | void zb_zcl_carbon_dioxide_init_server() 21 | { 22 | zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE, 23 | ZB_ZCL_CLUSTER_SERVER_ROLE, 24 | (zb_zcl_cluster_check_value_t)NULL, 25 | (zb_zcl_cluster_write_attr_hook_t)NULL, 26 | (zb_zcl_cluster_handler_t)NULL); 27 | } 28 | 29 | #define ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_SERVER_ROLE_INIT zb_zcl_carbon_dioxide_init_server 30 | #define ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_CLIENT_ROLE_INIT ((zb_zcl_cluster_init_t)NULL) 31 | 32 | 33 | typedef void * zb_voidp_t; 34 | #define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_CARBON_DIOXIDE_VALUE_ID(data_ptr) \ 35 | { \ 36 | ZB_ZCL_ATTR_CARBON_DIOXIDE_VALUE_ID, \ 37 | ZB_ZCL_ATTR_TYPE_SINGLE, \ 38 | ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_REPORTING, \ 39 | (ZB_UINT16_MAX), \ 40 | (zb_voidp_t) data_ptr \ 41 | } 42 | 43 | #define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_CARBON_DIOXIDE_MIN_VALUE_ID(data_ptr) \ 44 | { \ 45 | ZB_ZCL_ATTR_CARBON_DIOXIDE_MIN_VALUE_ID, \ 46 | ZB_ZCL_ATTR_TYPE_SINGLE, \ 47 | ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ 48 | (ZB_UINT16_MAX), \ 49 | (zb_voidp_t) data_ptr \ 50 | } 51 | 52 | #define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_CARBON_DIOXIDE_MAX_VALUE_ID(data_ptr) \ 53 | { \ 54 | ZB_ZCL_ATTR_CARBON_DIOXIDE_MAX_VALUE_ID, \ 55 | ZB_ZCL_ATTR_TYPE_SINGLE, \ 56 | ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ 57 | (ZB_UINT16_MAX), \ 58 | (zb_voidp_t) data_ptr \ 59 | } 60 | 61 | #define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_CARBON_DIOXIDE_TOLERANCE_ID(data_ptr) \ 62 | { \ 63 | ZB_ZCL_ATTR_CARBON_DIOXIDE_TOLERANCE_ID, \ 64 | ZB_ZCL_ATTR_TYPE_SINGLE, \ 65 | ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ 66 | (ZB_UINT16_MAX), \ 67 | (zb_voidp_t) data_ptr \ 68 | } 69 | 70 | #define ZB_ZCL_DECLARE_CARBON_DIOXIDE_ATTRIB_LIST(attr_list, \ 71 | value, min_value, max_value, tolerance) \ 72 | ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_CARBON_DIOXIDE) \ 73 | ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_CARBON_DIOXIDE_VALUE_ID, (value)) \ 74 | ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_CARBON_DIOXIDE_MIN_VALUE_ID, (min_value)) \ 75 | ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_CARBON_DIOXIDE_MAX_VALUE_ID, (max_value)) \ 76 | ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_CARBON_DIOXIDE_TOLERANCE_ID, (tolerance)) \ 77 | ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST 78 | 79 | {{end}} 80 | 81 | {{ define "carbon_dioxide_attr_list" }} 82 | ZB_ZCL_DECLARE_CARBON_DIOXIDE_ATTRIB_LIST( 83 | {{.Cluster.CVarName}}_{{.Endpoint}}_attr_list, 84 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.measure_value, 85 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.min_measure_value, 86 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.max_measure_value, 87 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.tolerance 88 | ); 89 | {{ end }} 90 | 91 | {{ define "carbon_dioxide_attr_init"}} 92 | /* Carbon Dioxide */ 93 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.measure_value = ZB_ZCL_ATTR_TEMP_MEASUREMENT_VALUE_UNKNOWN; 94 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.min_measure_value = ({{.Cluster.MinMeasuredValue}} * ZCL_CARBON_DIOXIDE_MEASURED_VALUE_MULTIPLIER); 95 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.max_measure_value = ({{.Cluster.MaxMeasuredValue}} * ZCL_CARBON_DIOXIDE_MEASURED_VALUE_MULTIPLIER); 96 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.tolerance = ({{.Cluster.Tolerance}} * ZCL_CARBON_DIOXIDE_MEASURED_VALUE_MULTIPLIER); 97 | {{end}} -------------------------------------------------------------------------------- /templates/src/zigbee/clusters.hpp.tpl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | #include 8 | 9 | #ifdef __cplusplus 10 | } 11 | #endif 12 | 13 | #define MANUFACTURER_CODE ZB_ZCL_MANUF_CODE_INVALID 14 | 15 | /* Temperature sensor device version */ 16 | #define ZB_HA_DEVICE_VER_TEMPERATURE_SENSOR 0 17 | 18 | /* Zigbee Cluster Library 4.4.2.2.1.1: MeasuredValue = 100x temperature in degrees Celsius */ 19 | #define ZCL_TEMPERATURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER 100 20 | /* Zigbee Cluster Library 4.5.2.2.1.1: MeasuredValue = 10x pressure in kPa */ 21 | #define ZCL_PRESSURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER 10 22 | /* Zigbee Cluster Library 4.7.2.1.1: MeasuredValue = 100x water content in % */ 23 | #define ZCL_HUMIDITY_MEASUREMENT_MEASURED_VALUE_MULTIPLIER 100 24 | 25 | {{ range $i, $cluster := .Device.Sensors.UniqueClusters }} 26 | {{ maybeRender (clusterTpl $cluster.ID "defines") (clusterCtx $i $cluster)}} 27 | {{end}} 28 | 29 | {{- range $i, $sensor := .Device.Sensors }} 30 | {{ $endpointID := (sum $i 1)}} 31 | {{ $inClustersNum := $sensor.Clusters.Servers }} 32 | {{if eq $i 0 }}{{ $inClustersNum = sum $inClustersNum 1 }}{{end}} 33 | // Define a cluster array for a single endpoint 34 | #define ZB_HA_DECLARE_DEVICE_CLUSTER_LIST_EP_{{$endpointID}}( \ 35 | cluster_list_name, \ 36 | {{- $clustersLen := len $sensor.Clusters}} 37 | {{- range $i, $cluster := $sensor.Clusters }} 38 | {{ $cluster.CVarName }}_attr_list{{if not (isLast $i $clustersLen)}},{{end}} \ 39 | {{- end }} 40 | ) \ 41 | zb_zcl_cluster_desc_t cluster_list_name[] = \ 42 | { \ 43 | {{- range $sensor.Clusters}} 44 | ZB_ZCL_CLUSTER_DESC( \ 45 | {{.ID.ToZCL}}, \ 46 | ZB_ZCL_ARRAY_SIZE({{.CVarName}}_attr_list, zb_zcl_attr_t), \ 47 | ({{.CVarName}}_attr_list), \ 48 | {{.Side.String}}, \ 49 | MANUFACTURER_CODE \ 50 | ), \ 51 | {{- end}} 52 | } 53 | {{- end }} 54 | 55 | {{- range $i, $sensor := .Device.Sensors}} 56 | {{ $endpointID := (sum $i 1)}} 57 | {{ $inClusterNum := $sensor.Clusters.Servers}} 58 | {{ $outClusterNum := $sensor.Clusters.Clients}} 59 | // Define an endpoint information(num of input&output, cluster IDs) 60 | #define ZB_ZCL_DECLARE_DEVICE_DESC_EP_{{$endpointID}}(ep_name) \ 61 | ZB_DECLARE_SIMPLE_DESC_VA({{$inClusterNum}}, {{$outClusterNum}}, EP_{{$endpointID}}); \ 62 | ZB_AF_SIMPLE_DESC_TYPE_VA({{$inClusterNum}}, {{$outClusterNum}}, EP_{{$endpointID}}) simple_desc_##ep_name = \ 63 | { \ 64 | {{$endpointID}}, \ 65 | ZB_AF_HA_PROFILE_ID, \ 66 | ZB_HA_TEMPERATURE_SENSOR_DEVICE_ID, /*This values are present as initial ones.*/ \ 67 | ZB_HA_DEVICE_VER_TEMPERATURE_SENSOR, /*This values are present as initial ones.*/ \ 68 | 0, \ 69 | {{$inClusterNum}}, \ 70 | {{$outClusterNum}}, \ 71 | { \ 72 | {{- range $sensor.Clusters}} 73 | {{.ID.ToZCL}}, \ 74 | {{- end}} 75 | } \ 76 | } 77 | {{- end }} 78 | 79 | // Define a single endpoint. 80 | // `ep_name` is variable that will be created, and will be later 81 | // used in `ZBOSS_DECLARE_DEVICE_CTX...` macros. 82 | // `cluster_list` is variable created by `ZB_HA_DECLARE_DEVICE_CLUSTER_LIST_...`. 83 | #define ZB_HA_DECLARE_DEVICE_EP(ep_name, ep_id, report_count, cluster_list) \ 84 | ZB_ZCL_DECLARE_DEVICE_DESC_EP_##ep_id(ep_name); \ 85 | ZBOSS_DEVICE_DECLARE_REPORTING_CTX( \ 86 | reporting_info##ep_name, \ 87 | report_count); \ 88 | ZB_AF_DECLARE_ENDPOINT_DESC( \ 89 | ep_name, \ 90 | ep_id, \ 91 | ZB_AF_HA_PROFILE_ID, \ 92 | 0, \ 93 | NULL, \ 94 | ZB_ZCL_ARRAY_SIZE(cluster_list, zb_zcl_cluster_desc_t), \ 95 | cluster_list, \ 96 | (zb_af_simple_desc_1_1_t *)&simple_desc_##ep_name, \ 97 | report_count, reporting_info##ep_name, 0, NULL) 98 | 99 | 100 | {{ if gt (len .Device.Sensors) 4}} 101 | // Register endpoints with device ctx 102 | // This macro is only for devices that have >4 endpoints 103 | #define ZBOSS_DECLARE_DEVICE_CTX_{{len .Device.Sensors}}_EP( \ 104 | device_ctx_name, \ 105 | {{- $sensorsLen := len .Device.Sensors }} 106 | {{- range $i, $_ := .Device.Sensors}} 107 | ep{{sum $i 1}}_name{{if not (isLast $i $sensorsLen)}},{{end}} \ 108 | {{- end}} 109 | ) \ 110 | ZB_AF_START_DECLARE_ENDPOINT_LIST(ep_list_##device_ctx_name) \ 111 | {{- range $i, $_ := .Device.Sensors}} 112 | &ep{{sum $i 1}}_name, \ 113 | {{- end}} 114 | ZB_AF_FINISH_DECLARE_ENDPOINT_LIST; \ 115 | ZBOSS_DECLARE_DEVICE_CTX(device_ctx_name, ep_list_##device_ctx_name, \ 116 | (ZB_ZCL_ARRAY_SIZE(ep_list_##device_ctx_name, zb_af_endpoint_desc_t*))) 117 | {{ end }} 118 | -------------------------------------------------------------------------------- /templates/src/zigbee/device_ctx.c.tpl: -------------------------------------------------------------------------------- 1 | {{define "device_ctx"}} 2 | 3 | {{ template "additional_types" . }} 4 | 5 | typedef struct { 6 | zb_zcl_basic_attrs_ext_t basic_attr; 7 | // zb_zcl_identify_attrs_t identify_attr; 8 | 9 | {{- range $i, $sensor := .}} 10 | {{- range .Clusters }} 11 | {{ .CAttrType }} {{.CVarName}}_{{$i}}_attrs; 12 | {{- end }} 13 | {{- end }} 14 | } zb_device_ctx; 15 | 16 | {{ end }} -------------------------------------------------------------------------------- /templates/src/zigbee/device_temperature.c.tpl: -------------------------------------------------------------------------------- 1 | {{ define "device_temp_config_attr_types"}} 2 | // ZCL spec 3.4.1.1 3 | #define ZB_ZCL_DEVICE_TEMP_CONFIG_CLUSTER_REVISION_DEFAULT ((zb_uint16_t)0x0001u) 4 | 5 | #define ZB_ZCL_ATTR_DEVICE_TEMP_CONFIG_VALUE_UNKNOWN ZB_ZCL_ATTR_PRESSURE_MEASUREMENT_VALUE_UNKNOWN 6 | 7 | /*! @brief CurrentTemperature, ZCL spec 3.4.2.2.1 */ 8 | #define ZB_ZCL_ATTR_DEVICE_TEMP_CONFIG_CURRENT_TEMPERATURE_ID (0x0000) 9 | // ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_CARBON_DIOXIDE_MIN_VALUE_ID 10 | void zb_zcl_device_temp_config_init_server() 11 | { 12 | zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_DEVICE_TEMP_CONFIG, 13 | ZB_ZCL_CLUSTER_SERVER_ROLE, 14 | (zb_zcl_cluster_check_value_t)NULL, 15 | (zb_zcl_cluster_write_attr_hook_t)NULL, 16 | (zb_zcl_cluster_handler_t)NULL); 17 | } 18 | 19 | #define ZB_ZCL_CLUSTER_ID_DEVICE_TEMP_CONFIG_SERVER_ROLE_INIT zb_zcl_device_temp_config_init_server 20 | #define ZB_ZCL_CLUSTER_ID_DEVICE_TEMP_CONFIG_CLIENT_ROLE_INIT ((zb_zcl_cluster_init_t)NULL) 21 | 22 | 23 | typedef void * zb_voidp_t; 24 | #define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_DEVICE_TEMP_CONFIG_CURRENT_TEMPERATURE_ID(data_ptr) \ 25 | { \ 26 | ZB_ZCL_ATTR_DEVICE_TEMP_CONFIG_CURRENT_TEMPERATURE_ID, \ 27 | ZB_ZCL_ATTR_TYPE_U16, \ 28 | ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ 29 | (ZB_UINT16_MAX), \ 30 | (zb_voidp_t) data_ptr \ 31 | } 32 | 33 | #define ZB_ZCL_DECLARE_DEVICE_TEMP_CONFIG_ATTRIB_LIST(attr_list, \ 34 | current_temperature) \ 35 | ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_DEVICE_TEMP_CONFIG) \ 36 | ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_DEVICE_TEMP_CONFIG_CURRENT_TEMPERATURE_ID, (current_temperature)) \ 37 | ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST 38 | 39 | {{end}} 40 | 41 | {{ define "device_temp_config_attr_list" }} 42 | ZB_ZCL_DECLARE_DEVICE_TEMP_CONFIG_ATTRIB_LIST( 43 | {{.Cluster.CVarName}}_{{.Endpoint}}_attr_list, 44 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.current_temperature 45 | ); 46 | {{ end }} 47 | 48 | {{ define "device_temp_config_attr_init"}} 49 | /* Device temperature */ 50 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.current_temperature = ZB_ZCL_ATTR_DEVICE_TEMP_CONFIG_VALUE_UNKNOWN; 51 | {{end}} -------------------------------------------------------------------------------- /templates/src/zigbee/ias_zone.c.tpl: -------------------------------------------------------------------------------- 1 | {{ define "ias_zone_attr_list" }} 2 | // For some reason non-extended version did not work 3 | // for me, but extended does work fine. 4 | ZB_ZCL_DECLARE_IAS_ZONE_ATTRIB_LIST_EXT( 5 | {{.Cluster.CVarName}}_{{.Endpoint}}_attr_list, 6 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.zone_state, 7 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.zone_type, 8 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.zone_status, 9 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.number_of_zone_sens_levels_supported, 10 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.current_zone_sens_level, 11 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.ias_cie_address, 12 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.zone_id, 13 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.cie_short_addr, 14 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.cie_ep 15 | ); 16 | {{end}} 17 | 18 | {{ define "ias_zone_attr_init"}} 19 | /* IAS Zone */ 20 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.zone_state = ZB_ZCL_IAS_ZONE_ZONESTATE_DEF_VALUE; 21 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.zone_type = {{.Cluster.ZoneType}}; 22 | // Set status to include Restore, to specify that 23 | // contact sensor does know when it is closed & open. 24 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.zone_status = ZB_ZCL_IAS_ZONE_ZONE_STATUS_RESTORE; 25 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.number_of_zone_sens_levels_supported = ZB_ZCL_IAS_ZONE_NUMBER_OF_ZONE_SENSITIVITY_LEVELS_SUPPORTED_DEFAULT_VALUE; 26 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.current_zone_sens_level = ZB_ZCL_IAS_ZONE_CURRENT_ZONE_SENSITIVITY_LEVEL_DEFAULT_VALUE; 27 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.zone_id = ZB_ZCL_IAS_ZONEID_ID_DEF_VALUE; 28 | {{end}} -------------------------------------------------------------------------------- /templates/src/zigbee/identify.c.tpl: -------------------------------------------------------------------------------- 1 | {{ define "identify_attr_list" }} 2 | ZB_ZCL_DECLARE_IDENTIFY_CLIENT_ATTRIB_LIST( 3 | {{.Cluster.CVarName}}_client_attr_list); 4 | 5 | ZB_ZCL_DECLARE_IDENTIFY_SERVER_ATTRIB_LIST( 6 | {{.Cluster.CVarName}}_server_attr_list, 7 | &dev_ctx.{{.Cluster.CVarName}}_attr.identify_time); 8 | {{ end }} -------------------------------------------------------------------------------- /templates/src/zigbee/on_off.c.tpl: -------------------------------------------------------------------------------- 1 | {{ define "on_off_attr_list" }} 2 | ZB_ZCL_DECLARE_ON_OFF_ATTRIB_LIST( 3 | {{.Cluster.CVarName}}_{{.Endpoint}}_attr_list, 4 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.on_off); 5 | {{ end }} -------------------------------------------------------------------------------- /templates/src/zigbee/power_config.c.tpl: -------------------------------------------------------------------------------- 1 | {{define "power_config_attr_types"}} 2 | // Define fixed attr list, as normal list does not contain 3 | // battery percentage, while extended is bugged: 4 | // does not provide `batt_num` value to macro. 5 | #define ZB_ZCL_DECLARE_POWER_CONFIG_BATTERY_ATTRIB_LIST(attr_list, voltage, rated_voltage, alarm_mask, voltage_min_threshold, percentage_remaining) \ 6 | ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_POWER_CONFIG) \ 7 | ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_VOLTAGE_ID(voltage, ), \ 8 | ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_RATED_VOLTAGE_ID(rated_voltage, ), \ 9 | ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_ALARM_MASK_ID(alarm_mask, ), \ 10 | ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_VOLTAGE_MIN_THRESHOLD_ID(voltage_min_threshold, ), \ 11 | ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_POWER_CONFIG_BATTERY_PERCENTAGE_REMAINING_ID(percentage_remaining, ), \ 12 | ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST 13 | {{end}} 14 | 15 | {{ define "power_config_attr_list" }} 16 | ZB_ZCL_DECLARE_POWER_CONFIG_BATTERY_ATTRIB_LIST( 17 | {{.Cluster.CVarName}}_{{.Endpoint}}_attr_list, 18 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.voltage, 19 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.rated_voltage, 20 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.alarm_mask, 21 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.voltage_min_threshold, 22 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.percentage_remaining); 23 | {{ end }} 24 | 25 | {{ define "power_config_attr_init"}} 26 | /* IAS Zone */ 27 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.voltage = ZB_ZCL_POWER_CONFIG_BATTERY_VOLTAGE_INVALID; 28 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.rated_voltage = {{.Cluster.BatteryRatedVoltage | formatHex}}; 29 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.alarm_mask = ZB_ZCL_POWER_CONFIG_BATTERY_ALARM_MASK_DEFAULT_VALUE; 30 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.voltage_min_threshold = {{.Cluster.BatteryVoltageMinThreshold | formatHex}}; 31 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.percentage_remaining = ZB_ZCL_POWER_CONFIG_BATTERY_REMAINING_UNKNOWN; 32 | {{end}} -------------------------------------------------------------------------------- /templates/src/zigbee/pressure.c.tpl: -------------------------------------------------------------------------------- 1 | {{ define "pressure_attr_list" }} 2 | ZB_ZCL_DECLARE_PRESSURE_MEASUREMENT_ATTRIB_LIST( 3 | {{.Cluster.CVarName}}_{{.Endpoint}}_attr_list, 4 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.measure_value, 5 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.min_measure_value, 6 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.max_measure_value, 7 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.tolerance 8 | ); 9 | {{ end }} 10 | 11 | {{ define "pressure_attr_init"}} 12 | /* Pressure */ 13 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.measure_value = ZB_ZCL_ATTR_PRESSURE_MEASUREMENT_VALUE_UNKNOWN; 14 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.min_measure_value = ({{.Cluster.MinMeasuredValue}} * ZCL_PRESSURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER); 15 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.max_measure_value = ({{.Cluster.MaxMeasuredValue}} * ZCL_PRESSURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER); 16 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.tolerance = ({{.Cluster.Tolerance}} * ZCL_PRESSURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER); 17 | {{end}} -------------------------------------------------------------------------------- /templates/src/zigbee/temperature.c.tpl: -------------------------------------------------------------------------------- 1 | {{ define "temperature_attr_list" }} 2 | ZB_ZCL_DECLARE_TEMP_MEASUREMENT_ATTRIB_LIST( 3 | {{.Cluster.CVarName}}_{{.Endpoint}}_attr_list, 4 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.measure_value, 5 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.min_measure_value, 6 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.max_measure_value, 7 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.tolerance 8 | ); 9 | {{ end }} 10 | 11 | {{ define "temperature_attr_init"}} 12 | /* Temperature */ 13 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.measure_value = ZB_ZCL_ATTR_TEMP_MEASUREMENT_VALUE_UNKNOWN; 14 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.min_measure_value = ({{.Cluster.MinMeasuredValue}} * ZCL_TEMPERATURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER); 15 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.max_measure_value = ({{.Cluster.MaxMeasuredValue}} * ZCL_TEMPERATURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER); 16 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.tolerance = ({{.Cluster.Tolerance}} * ZCL_TEMPERATURE_MEASUREMENT_MEASURED_VALUE_MULTIPLIER); 17 | {{ end}} -------------------------------------------------------------------------------- /templates/src/zigbee/water_content.tpl: -------------------------------------------------------------------------------- 1 | {{ define "water_content_defines"}} 2 | {{ if (eq .Cluster.ID.ToName "soil_moisture") }} 3 | // ZCL spec 4.7.1.3 4 | #define ZB_ZCL_CLUSTER_ID_SOIL_MOISTURE (0x0408) 5 | 6 | // ZCL spec 4.7.1.1 7 | #define ZB_ZCL_SOIL_MOISTURE_CLUSTER_REVISION_DEFAULT ((zb_uint16_t)0x0002u) 8 | 9 | #define ZB_ZCL_ATTR_SOIL_MOISTURE_VALUE_UNKNOWN (0xffff) 10 | 11 | {{end}} 12 | {{ end }} 13 | 14 | {{ define "water_content_attr_types"}} 15 | {{ if (eq .Cluster.ID.ToName "soil_moisture") }} 16 | void zb_zcl_soil_moisture_init_server() 17 | { 18 | zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_SOIL_MOISTURE, 19 | ZB_ZCL_CLUSTER_SERVER_ROLE, 20 | (zb_zcl_cluster_check_value_t)NULL, 21 | (zb_zcl_cluster_write_attr_hook_t)NULL, 22 | (zb_zcl_cluster_handler_t)NULL); 23 | } 24 | 25 | #define ZB_ZCL_CLUSTER_ID_SOIL_MOISTURE_SERVER_ROLE_INIT zb_zcl_soil_moisture_init_server 26 | #define ZB_ZCL_CLUSTER_ID_SOIL_MOISTURE_CLIENT_ROLE_INIT ((zb_zcl_cluster_init_t)NULL) 27 | {{end}} 28 | {{end}} 29 | 30 | {{ define "water_content_attr_list" }} 31 | ZB_ZCL_DECLARE_REL_HUMIDITY_MEASUREMENT_ATTRIB_LIST( 32 | {{.Cluster.CVarName}}_{{.Endpoint}}_attr_list, 33 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.measure_value, 34 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.min_measure_value, 35 | &dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.max_measure_value 36 | ); 37 | {{ end }} 38 | 39 | {{define "water_content_attr_init"}} 40 | /* Water content, {{.Cluster.CVarName}} */ 41 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.measure_value = ZB_ZCL_ATTR_REL_HUMIDITY_MEASUREMENT_VALUE_UNKNOWN; 42 | // Range is 0 - 100% 43 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.min_measure_value = 0; 44 | dev_ctx.{{.Cluster.CVarName}}_{{.Endpoint}}_attrs.max_measure_value = 100 * 100; 45 | /* Humidity measurements tolerance is not supported at the moment */ 46 | {{end}} -------------------------------------------------------------------------------- /templates/template_tree.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "text/template" 7 | ) 8 | 9 | type templateTree struct { 10 | tree map[string]*templateTree 11 | tpl *template.Template 12 | } 13 | 14 | func (t *templateTree) FindByPath(parts ...string) []*template.Template { 15 | currentTree := t 16 | for _, part := range parts { 17 | currentTree = currentTree.tree[part] 18 | if currentTree == nil { 19 | panic(fmt.Sprintf("part %q of prefix %q is not present in template tree", part, filepath.Join(parts...))) 20 | } 21 | } 22 | 23 | return fetchTemplatesFromTree(currentTree, nil) 24 | } 25 | 26 | func fetchTemplatesFromTree(tree *templateTree, templates []*template.Template) []*template.Template { 27 | for _, innerTree := range tree.tree { 28 | templates = fetchTemplatesFromTree(innerTree, templates) 29 | } 30 | 31 | if tree.tpl != nil { 32 | templates = append(templates, tree.tpl) 33 | } 34 | 35 | return templates 36 | } 37 | -------------------------------------------------------------------------------- /types/appconfig/appconfig.go: -------------------------------------------------------------------------------- 1 | package appconfig 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sort" 7 | "strconv" 8 | ) 9 | 10 | type Provider interface { 11 | AppConfig() []ConfigValue 12 | } 13 | 14 | type ConfigValue struct { 15 | Name string 16 | DefaultValue string 17 | RequiredValue string 18 | QuotedValue bool 19 | 20 | Dependencies []ConfigValue 21 | } 22 | 23 | type AppConfig struct { 24 | values map[string]ConfigValue 25 | } 26 | 27 | func NewValue(name string) ConfigValue { 28 | return ConfigValue{Name: name} 29 | } 30 | 31 | func (v ConfigValue) Default(val string) ConfigValue { 32 | v.DefaultValue = val 33 | return v 34 | } 35 | 36 | func (v ConfigValue) Required(val string) ConfigValue { 37 | v.RequiredValue = val 38 | return v 39 | } 40 | 41 | func (v ConfigValue) Quoted() ConfigValue { 42 | v.QuotedValue = true 43 | return v 44 | } 45 | 46 | func (v ConfigValue) Value() string { 47 | returnValue := v.DefaultValue 48 | 49 | if v.RequiredValue != "" { 50 | returnValue = v.RequiredValue 51 | } 52 | 53 | if v.QuotedValue { 54 | returnValue = `"` + returnValue + `"` 55 | } 56 | 57 | return returnValue 58 | } 59 | 60 | func (v ConfigValue) Depends(cfgs ...ConfigValue) ConfigValue { 61 | v = v.Copy() 62 | 63 | for i := range cfgs { 64 | // Make value required, as we *require* it as a dependency 65 | cfgs[i].RequiredValue = cfgs[i].Value() 66 | } 67 | 68 | v.Dependencies = append(v.Dependencies, cfgs...) 69 | return v 70 | } 71 | 72 | func (v ConfigValue) Copy() ConfigValue { 73 | return ConfigValue{ 74 | Name: v.Name, 75 | DefaultValue: v.DefaultValue, 76 | RequiredValue: v.RequiredValue, 77 | 78 | Dependencies: append([]ConfigValue(nil), v.Dependencies...), 79 | } 80 | } 81 | 82 | func NewEmptyAppConfig() *AppConfig { 83 | return &AppConfig{ 84 | values: make(map[string]ConfigValue), 85 | } 86 | } 87 | 88 | type DefaultAppConfigOptions struct { 89 | IsRouter bool 90 | ZigbeeChannels []int 91 | } 92 | 93 | func NewDefaultAppConfig(opts DefaultAppConfigOptions) (*AppConfig, error) { 94 | appConfig := NewEmptyAppConfig().AddValue( 95 | CONFIG_CPP, 96 | CONFIG_DK_LIBRARY, 97 | CONFIG_ZIGBEE, 98 | CONFIG_ZIGBEE_APP_UTILS, 99 | CONFIG_ZIGBEE_CHANNEL_MASK, 100 | CONFIG_ZIGBEE_CHANNEL_SELECTION_MODE_MULTI, 101 | CONFIG_CRYPTO, 102 | CONFIG_CRYPTO_NRF_ECB, 103 | CONFIG_CRYPTO_INIT_PRIORITY, 104 | CONFIG_RAM_POWER_DOWN_LIBRARY, 105 | CONFIG_NET_IPV6, 106 | CONFIG_NET_IP_ADDR_CHECK, 107 | CONFIG_NET_UDP, 108 | CONFIG_CONSOLE, 109 | CONFIG_USB_DEVICE_STACK, 110 | ) 111 | 112 | deviceRole := CONFIG_ZIGBEE_ROLE_END_DEVICE 113 | if opts.IsRouter { 114 | deviceRole = CONFIG_ZIGBEE_ROLE_ROUTER 115 | } 116 | 117 | appConfig = appConfig.AddValue(deviceRole) 118 | 119 | if len(opts.ZigbeeChannels) != 0 { 120 | channel := int32(0) 121 | 122 | for _, chann := range opts.ZigbeeChannels { 123 | if chann < 11 || chann > 26 { 124 | return nil, fmt.Errorf("zigbee channels must be in range [11, 26], but have %d", chann) 125 | } 126 | 127 | channel |= 1 << chann 128 | } 129 | 130 | appConfig = appConfig.AddValue(CONFIG_ZIGBEE_CHANNEL_MASK.Required("0x" + strconv.FormatInt(int64(channel), 16))) 131 | } 132 | 133 | return appConfig, nil 134 | } 135 | 136 | func (c *AppConfig) AddValue(configValues ...ConfigValue) *AppConfig { 137 | for _, configValue := range configValues { 138 | // Only single-level dependencies for now. 139 | for _, dep := range configValue.Dependencies { 140 | val, ok := c.values[dep.Name] 141 | if !ok { 142 | c.values[dep.Name] = dep 143 | 144 | continue 145 | } 146 | 147 | if dep.RequiredValue != "" { 148 | if val.RequiredValue != "" && val.RequiredValue != dep.RequiredValue { 149 | panic(fmt.Sprintf("config value %q already has required value %q, but %q requires it to be %q", dep.Name, val.RequiredValue, configValue.Name, dep.RequiredValue)) 150 | } 151 | 152 | c.values[dep.Name] = dep 153 | 154 | continue 155 | } 156 | } 157 | 158 | if val, ok := c.values[configValue.Name]; ok { 159 | if val.RequiredValue != "" && 160 | configValue.RequiredValue != "" && 161 | configValue.RequiredValue != val.RequiredValue { 162 | panic(fmt.Sprintf("config value %q already has required value %q, but new added value requires it to be %q", val.Name, val.RequiredValue, configValue.RequiredValue)) 163 | } 164 | } 165 | 166 | c.values[configValue.Name] = configValue 167 | } 168 | 169 | return c 170 | } 171 | 172 | func (c *AppConfig) WriteTo(w io.StringWriter) error { 173 | configNames := make([]string, 0, len(c.values)) 174 | for name := range c.values { 175 | configNames = append(configNames, name) 176 | } 177 | sort.Strings(configNames) 178 | 179 | for _, name := range configNames { 180 | if _, err := w.WriteString(name + "=" + c.values[name].Value() + "\n"); err != nil { 181 | return fmt.Errorf("write to writer: %w", err) 182 | } 183 | } 184 | 185 | return nil 186 | } 187 | -------------------------------------------------------------------------------- /types/appconfig/doc.go: -------------------------------------------------------------------------------- 1 | // This package provides functionality to define 2 | // configuration option with their values, and also define 3 | // dependencies on other configuration options. 4 | // 5 | // Configuration values are not ordered in the output, 6 | // and dependencies are managed on very basic level. 7 | // 8 | // Configuration values already added to global list 9 | // for now are treated as required, which is not 10 | // necessarily correct. 11 | package appconfig 12 | -------------------------------------------------------------------------------- /types/appconfig/known.go: -------------------------------------------------------------------------------- 1 | package appconfig 2 | 3 | // NOTE: This values are fetched from original `prj.conf` of this project, 4 | // and some were updated to disable/remove some things as default. 5 | // As such they do not represent good/best configurations, 6 | // but mostly the ones that work for this project. 7 | var ( 8 | // Zephyr config 9 | CONFIG_CPP = NewValue("CONFIG_CPP").Default(Yes) 10 | 11 | // Logging 12 | CONFIG_LOG = NewValue("CONFIG_LOG").Default(No) 13 | CONFIG_SERIAL = NewValue("CONFIG_SERIAL").Default(No) 14 | CONFIG_CONSOLE = NewValue("CONFIG_CONSOLE").Default(No) 15 | CONFIG_UART_CONSOLE = NewValue("CONFIG_UART_CONSOLE").Default(No) 16 | CONFIG_UART_LINE_CTRL = NewValue("CONFIG_UART_LINE_CTRL").Default(Yes) 17 | CONFIG_LOG_BACKEND_UART = NewValue("CONFIG_LOG_BACKEND_UART").Default(Yes) 18 | CONFIG_PRINTK = NewValue("CONFIG_PRINTK").Default(Yes) 19 | 20 | // USB 21 | CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT = NewValue("CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT").Default(No) 22 | CONFIG_USB_DEVICE_PRODUCT = NewValue("CONFIG_USB_DEVICE_PRODUCT").Default(`"Dongle: Zigbee Device"`) 23 | CONFIG_USB_DEVICE_PID = NewValue("CONFIG_USB_DEVICE_PID").Default(`0x0004`) 24 | CONFIG_USB_DEVICE_STACK = NewValue("CONFIG_USB_DEVICE_STACK").Default(No) 25 | 26 | // Drivers / peripherals 27 | CONFIG_I2C = NewValue("CONFIG_I2C").Default(Yes) 28 | CONFIG_SENSOR = NewValue("CONFIG_SENSOR").Default(No) 29 | CONFIG_BME280 = NewValue("CONFIG_BME280").Default(No).Depends(CONFIG_I2C.Required(Yes), CONFIG_SENSOR.Required(Yes)) 30 | CONFIG_DK_LIBRARY = NewValue("CONFIG_DK_LIBRARY").Default(Yes) 31 | 32 | // Zigbee 33 | CONFIG_ZIGBEE = NewValue("CONFIG_ZIGBEE").Default(Yes) 34 | CONFIG_ZIGBEE_APP_UTILS = NewValue("CONFIG_ZIGBEE_APP_UTILS").Default(Yes) 35 | CONFIG_ZIGBEE_CHANNEL_MASK = NewValue("CONFIG_ZIGBEE_CHANNEL_MASK").Default("0x7FFF800") 36 | CONFIG_ZIGBEE_ROLE_END_DEVICE = NewValue("CONFIG_ZIGBEE_ROLE_END_DEVICE").Default(Yes) 37 | CONFIG_ZIGBEE_ROLE_ROUTER = NewValue("CONFIG_ZIGBEE_ROLE_ROUTER").Default(Yes) 38 | CONFIG_ZIGBEE_CHANNEL_SELECTION_MODE_MULTI = NewValue("CONFIG_ZIGBEE_CHANNEL_SELECTION_MODE_MULTI").Default(Yes) 39 | 40 | // Cryptography 41 | CONFIG_CRYPTO = NewValue("CONFIG_CRYPTO").Default(Yes) 42 | CONFIG_CRYPTO_NRF_ECB = NewValue("CONFIG_CRYPTO_NRF_ECB").Default(Yes).Depends(CONFIG_CRYPTO) 43 | CONFIG_CRYPTO_INIT_PRIORITY = NewValue("CONFIG_CRYPTO_INIT_PRIORITY").Default(`80`) 44 | 45 | // Power configuration 46 | CONFIG_RAM_POWER_DOWN_LIBRARY = NewValue("CONFIG_RAM_POWER_DOWN_LIBRARY").Default(Yes) 47 | 48 | // Network 49 | CONFIG_NET_IPV6 = NewValue("CONFIG_NET_IPV6").Default(No) 50 | CONFIG_NET_IP_ADDR_CHECK = NewValue("CONFIG_NET_IP_ADDR_CHECK").Default(No) 51 | CONFIG_NET_UDP = NewValue("CONFIG_NET_UDP").Default(No) 52 | 53 | // Sensors 54 | CONFIG_DHT = NewValue("CONFIG_DHT").Default(Yes) 55 | ) 56 | 57 | const ( 58 | Yes = "y" 59 | No = "n" 60 | ) 61 | -------------------------------------------------------------------------------- /types/board/known_boards.go: -------------------------------------------------------------------------------- 1 | package board 2 | 3 | import ( 4 | "slices" 5 | "strconv" 6 | 7 | "github.com/ffenix113/zigbee_home/types/appconfig" 8 | ) 9 | 10 | type Bootloader struct { 11 | PM []Section 12 | Config []appconfig.ConfigValue 13 | } 14 | 15 | const ( 16 | RegionFlashPrimary = "flash_primary" 17 | RegionSramPrimary = "sram_primary" 18 | ) 19 | 20 | type Section struct { 21 | Name string 22 | Address int 23 | Size int 24 | Region string 25 | } 26 | 27 | func (s Section) EndAddress() int { 28 | return s.Address + s.Size 29 | } 30 | 31 | func BootloaderConfigFromBoard(board string) (*Bootloader, string) { 32 | var boardBootloader string 33 | 34 | for bootloader, boards := range bootloaderBoards() { 35 | if _, found := slices.BinarySearch(boards, board); found { 36 | boardBootloader = bootloader 37 | 38 | break 39 | } 40 | } 41 | 42 | return BootloaderConfig(boardBootloader), boardBootloader 43 | } 44 | 45 | func BootloaderConfig(bootloader string) *Bootloader { 46 | // By default will return nil, if `bootloader` is empty. 47 | // This would be enough to not include any bootloader configuration 48 | return knownBootloaders()[bootloader] 49 | } 50 | 51 | func bootloaderBoards() map[string][]string { 52 | // Values in this map can be added in any order, 53 | // as they will be ordered on the next step. 54 | bootloadersMap := map[string][]string{ 55 | "arduino": { 56 | "arduino_nano_33_ble", 57 | }, 58 | "nrf52_legacy": { 59 | "nrf52840dongle_nrf52840", 60 | }, 61 | "adafruit_nrf52_sd132": {}, 62 | "adafruit_nrf52_sd140_v6": {}, 63 | "adafruit_nrf52_sd140_v7": { 64 | "xiao_ble", 65 | }, 66 | } 67 | 68 | for _, boards := range bootloadersMap { 69 | slices.Sort(boards) 70 | } 71 | 72 | return bootloadersMap 73 | } 74 | 75 | func knownBootloaders() map[string]*Bootloader { 76 | adafruitConfig := func(appStartAddr int) *Bootloader { 77 | appStartAddrHex := "0x" + strconv.FormatInt(int64(appStartAddr), 16) 78 | 79 | return &Bootloader{ 80 | PM: []Section{ 81 | { 82 | Name: "empty_app_offset", 83 | Address: 0x0, 84 | Size: appStartAddr, 85 | Region: RegionFlashPrimary, 86 | }, 87 | { 88 | // This section is needed because without it 89 | // the app will be from appStartAddr till 0xf7000, 90 | // which is right in the middle of bootloader. 91 | // Next section will be for zboss and it will start 92 | // at 0xf7000, which will overwrite part of bootloader 93 | // resulting in a dead board after power-cycle, 94 | // but can be resurected if bootloader is re-flashed. 95 | Name: "empty_after_zboss_offset", 96 | Address: 0xf4000, 97 | Size: 0xc000, 98 | Region: RegionFlashPrimary, 99 | }, 100 | }, 101 | Config: []appconfig.ConfigValue{ 102 | appconfig.NewValue("CONFIG_BUILD_OUTPUT_UF2").Required(appconfig.Yes), 103 | // This option is to prevent use of default flash load offset. 104 | // May be specific to some boards, but we need it just to be safe. 105 | appconfig.NewValue("CONFIG_BOOTLOADER_BOSSA").Required(appconfig.No), 106 | appconfig.NewValue("CONFIG_FLASH_LOAD_OFFSET").Required(appStartAddrHex), 107 | }, 108 | } 109 | } 110 | 111 | return map[string]*Bootloader{ 112 | // Legacy NRF52 bootloader, provided with nRF5 SDK. Also used in nRF52840 Dongle till now for some reason. 113 | "nrf52_legacy": { 114 | // This configuration is based on 115 | // https://github.com/martelmy/NCS_examples/blob/3cd874c68e13565ca89a082c67af54f48b704192/zigbee/light_bulb_dongle/pm_static_nrf52840dongle_nrf52840.yml 116 | // , based on the answer here: 117 | // https://devzone.nordicsemi.com/f/nordic-q-a/87169/zigbee-example-for-nrf52840-dongle/364086 118 | // 119 | // Thank you Marte Myrvold(https://github.com/martelmy) 120 | PM: []Section{ 121 | { 122 | Name: "empty_bootloader", 123 | Address: 0xe0000, 124 | Size: 0x20000, 125 | Region: RegionFlashPrimary, 126 | }, 127 | { 128 | Name: "empty_sram_bootloader", 129 | Address: 0x20000000, 130 | Size: 0x400, 131 | Region: RegionSramPrimary, 132 | }, 133 | }, 134 | }, 135 | // Bossac bootloader 136 | "arduino": { 137 | PM: []Section{ 138 | { 139 | Name: "empty_before_app", 140 | Address: 0x0, 141 | Size: 0x10000, 142 | Region: RegionFlashPrimary, 143 | }, 144 | // App is launching and starting to work without SRAM config, 145 | // so we will not define it(for now at least). 146 | // Maybe there will be issues in the future, and they will be fixed here. 147 | }, 148 | }, 149 | "adafruit_nrf52_sd132": adafruitConfig(0x26000), 150 | "adafruit_nrf52_sd140_v6": adafruitConfig(0x26000), 151 | "adafruit_nrf52_sd140_v7": adafruitConfig(0x27000), 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /types/devicetree/devicetree_test.go: -------------------------------------------------------------------------------- 1 | package devicetree_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/ffenix113/zigbee_home/types/devicetree" 8 | ) 9 | 10 | func TestWriteOverlay(t *testing.T) { 11 | deviceTree := (&devicetree.DeviceTree{}).AddNodes( 12 | &devicetree.Node{ 13 | Name: devicetree.NodeNameRoot, 14 | SubNodes: []*devicetree.Node{ 15 | { 16 | Name: devicetree.NodeNameChosen, 17 | Properties: []devicetree.Property{ 18 | devicetree.NewProperty("ncs,zigbee-timer", devicetree.Label("timer2")), 19 | devicetree.NewProperty("zephyr,console", devicetree.Label("cdc_acm_uart0")), 20 | devicetree.NewProperty("zephyr,entropy", devicetree.Label("rng")), 21 | }, 22 | }, 23 | }, 24 | }, 25 | &devicetree.Node{ 26 | Label: "zephyr_udc0", 27 | Upsert: true, 28 | SubNodes: []*devicetree.Node{ 29 | { 30 | Name: "cdc_acm_uart0", 31 | Label: "cdc_acm_uart0", 32 | Properties: []devicetree.Property{ 33 | devicetree.NewProperty(devicetree.PropertyNameCompatible, devicetree.Quoted("zephyr,cdc-acm-uart")), 34 | }, 35 | }, 36 | }, 37 | }, 38 | ) 39 | 40 | var buf strings.Builder 41 | 42 | deviceTree.WriteTo(&buf) 43 | 44 | t.Log(buf.String()) 45 | } 46 | -------------------------------------------------------------------------------- /types/devicetree/doc.go: -------------------------------------------------------------------------------- 1 | // This package provides reasonable functionality to define 2 | // overlay files for the boards. 3 | // 4 | // Primary purpose of this package is not to provide 5 | // complete implementation of [Devicetree](https://www.devicetree.org/), 6 | // but to allow features required by this project to be 7 | // defined in code and then generated to an overlay file. 8 | // 9 | // It is highly possible that this will remain as low-level 10 | // implementation of devicetree generator, while high-level 11 | // package/type with required node definitions will be added later. 12 | // 13 | // See also: https://docs.zephyrproject.org/3.4.0/build/dts/index.html 14 | package devicetree 15 | -------------------------------------------------------------------------------- /types/devicetree/known.go: -------------------------------------------------------------------------------- 1 | package devicetree 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/ffenix113/zigbee_home/types" 8 | ) 9 | 10 | type KnownNode interface { 11 | AttachSelf(dt *DeviceTree) error 12 | } 13 | 14 | // Verify that pin implements both interfaces 15 | var _ interface { 16 | LED 17 | Button 18 | } = pin{} 19 | 20 | type LED interface { 21 | KnownNode 22 | led() 23 | } 24 | 25 | type Button interface { 26 | KnownNode 27 | button() 28 | } 29 | 30 | type UART struct { 31 | Tx, Rx types.Pin 32 | } 33 | 34 | func (u UART) AttachSelf(dt *DeviceTree) error { 35 | pinctrlNode := dt.FindSpecificNode(SearchByLabel(NodeLabelPinctrl)) 36 | if pinctrlNode == nil { 37 | return ErrNodeNotFound(NodeLabelPinctrl) 38 | } 39 | 40 | pinctrlNode.AddNodes( 41 | &Node{ 42 | Name: "uart0_default", 43 | Label: "uart0_default", 44 | SubNodes: []*Node{ 45 | { 46 | Name: "group1", 47 | Properties: []Property{ 48 | NewProperty("psels", NrfPSel("UART_TX", u.Tx.Port.Value(), u.Tx.Pin.Value())), 49 | }, 50 | }, 51 | { 52 | Name: "group2", 53 | Properties: []Property{ 54 | NewProperty("psels", NrfPSel("UART_RX", u.Rx.Port.Value(), u.Rx.Pin.Value())), 55 | NewProperty("bias-pull-up", nil), 56 | }, 57 | }, 58 | }, 59 | }, 60 | ) 61 | 62 | pinctrlNode.AddNodes(&Node{ 63 | Name: "uart0_sleep", 64 | Label: "uart0_sleep", 65 | SubNodes: []*Node{ 66 | { 67 | Name: "group1", 68 | Properties: []Property{ 69 | NewProperty("psels", Array( 70 | NrfPSel("UART_TX", u.Tx.Port.Value(), u.Tx.Pin.Value()), 71 | NrfPSel("UART_RX", u.Rx.Port.Value(), u.Rx.Pin.Value()), 72 | )), 73 | NewProperty("low-power-enable", nil), 74 | }, 75 | }, 76 | }, 77 | }) 78 | 79 | return nil 80 | } 81 | 82 | func NewLED(ledPin types.Pin) LED { 83 | return pin{ 84 | Pin: ledPin, 85 | 86 | nodeName: "leds", 87 | compatible: "gpio-leds", 88 | } 89 | } 90 | 91 | func NewButton(btnPin types.Pin) Button { 92 | return pin{ 93 | Pin: btnPin, 94 | 95 | nodeName: "buttons", 96 | compatible: "gpio-keys", 97 | } 98 | } 99 | 100 | type pin struct { 101 | Pin types.Pin 102 | 103 | nodeName, compatible string 104 | } 105 | 106 | func (p pin) AttachSelf(dt *DeviceTree) error { 107 | pinName := p.Pin.ID 108 | if pinName == "" { 109 | pinName = p.Pin.Label() 110 | } 111 | 112 | aliases := dt.FindSpecificNode(SearchByName(NodeNameRoot), SearchByName(NodeNameAliases)) 113 | aliases.Properties = append(aliases.Properties, 114 | NewProperty(strings.ReplaceAll(pinName, "_", "-"), Label(pinName))) 115 | 116 | // If pin is not defined - do not add its configuration. 117 | if !p.Pin.PinsDefined() { 118 | return nil 119 | } 120 | 121 | pinsNode := dt.FindSpecificNode(SearchByName(NodeNameRoot), SearchByName(p.nodeName)) 122 | if pinsNode == nil { 123 | pinsNode = &Node{ 124 | Name: p.nodeName, 125 | Label: p.nodeName, 126 | Properties: []Property{ 127 | NewProperty(PropertyNameCompatible, FromValue(p.compatible)), 128 | }, 129 | } 130 | 131 | dt.FindSpecificNode(SearchByName(NodeNameRoot)).AddNodes(pinsNode) 132 | } 133 | 134 | activeState := "GPIO_ACTIVE_HIGH" 135 | if p.Pin.Inverted { 136 | activeState = "GPIO_ACTIVE_LOW" 137 | } 138 | 139 | pinsNode.AddNodes(&Node{ 140 | Name: pinName, 141 | Label: pinName, 142 | Properties: []Property{ 143 | NewProperty("gpios", Angled(Label(fmt.Sprintf("gpio%d %d %s", p.Pin.Port, p.Pin.Pin, activeState)))), 144 | }, 145 | }) 146 | 147 | return nil 148 | } 149 | 150 | func (pin) led() {} 151 | func (pin) button() {} 152 | -------------------------------------------------------------------------------- /types/devicetree/property.go: -------------------------------------------------------------------------------- 1 | package devicetree 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | var PropertyStatusEnable = NewProperty(PropertyNameStatus, StatusOkay) 10 | var PropertyStatusDisable = NewProperty(PropertyNameStatus, StatusDisabled) 11 | 12 | const StatusOkay = rawValue(`"okay"`) 13 | const StatusDisabled = rawValue(`"disabled"`) 14 | 15 | const PropertyNameCompatible = "compatible" 16 | const PropertyNameStatus = "status" 17 | 18 | type Property struct { 19 | Name string 20 | Value PropertyValue 21 | } 22 | 23 | func NewProperty(name string, value PropertyValue) Property { 24 | return Property{name, value} 25 | } 26 | 27 | func (p Property) writeTo(w io.StringWriter) error { 28 | w.WriteString(p.Name) 29 | 30 | if p.Value != nil { 31 | w.WriteString(" = " + p.Value.Value()) 32 | } 33 | 34 | w.WriteString(";") 35 | 36 | return nil 37 | } 38 | 39 | type PropertyValue interface { 40 | Value() string 41 | } 42 | 43 | type rawValue string 44 | 45 | func (v rawValue) Value() string { 46 | return string(v) 47 | } 48 | 49 | func PropertyValueFn(fn func() string) PropertyValue { 50 | return rawValue(fn()) 51 | } 52 | 53 | func String(value string) PropertyValue { 54 | return rawValue(value) 55 | } 56 | 57 | func Quoted(value string) PropertyValue { 58 | return rawValue(`"` + value + `"`) 59 | } 60 | 61 | func Label(label string) PropertyValue { 62 | return rawValue("&" + label) 63 | } 64 | 65 | // NrfPSel 66 | // Reference: https://docs.zephyrproject.org/apidoc/latest/nrf-pinctrl_8h.html 67 | func NrfPSel(fun string, port, pin uint8) PropertyValue { 68 | formatted := fmt.Sprintf("NRF_PSEL(%s, %d, %d)", fun, port, pin) 69 | 70 | return Angled(rawValue(formatted)) 71 | } 72 | 73 | func Angled(value PropertyValue) PropertyValue { 74 | return rawValue("<" + value.Value() + ">") 75 | } 76 | 77 | func Array(values ...PropertyValue) PropertyValue { 78 | if len(values) < 2 { 79 | panic("array must have at least two values") 80 | } 81 | 82 | parts := make([]string, 0, len(values)) 83 | 84 | for _, value := range values { 85 | parts = append(parts, value.Value()) 86 | } 87 | 88 | return rawValue(strings.Join(parts, ", ")) 89 | } 90 | 91 | func FromValue(val any) PropertyValue { 92 | switch typed := val.(type) { 93 | case string: 94 | return Quoted(typed) 95 | case uint8, int, int8: 96 | return Angled(rawValue(fmt.Sprintf("%d", val))) 97 | } 98 | 99 | panic(fmt.Sprintf("unknown type to convert to property value: %T", val)) 100 | } 101 | -------------------------------------------------------------------------------- /types/generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/ffenix113/zigbee_home/types/appconfig" 8 | "github.com/ffenix113/zigbee_home/types/devicetree" 9 | ) 10 | 11 | type Adder interface { 12 | AppConfig() []appconfig.ConfigValue 13 | ApplyOverlay(overlay *devicetree.DeviceTree) error 14 | } 15 | 16 | type WriteFile struct { 17 | FileName string 18 | TemplateName string 19 | AdditionalContext any 20 | } 21 | 22 | // Extender provides a way to extend source code by writing new 23 | // files, add code to main.c and extend CMakeLists.txt 24 | // 25 | // Each unique extender will be executed only once. 26 | // Uniqueness of extender is determined either by package & type name, 27 | // or by `Name`, if extender is instance of `SimpleExtender`. 28 | type Extender interface { 29 | // Includes returns include paths that will be 30 | // added after all pre-defined includes in main.c 31 | Includes() []string 32 | // Template returns template path that will define 33 | // known extender steps. 34 | Template() string 35 | // WriteFiles returns a slice of files that should be written 36 | // from the template names. Necessary paths will be 37 | // created if file is located in directory that does not yet exist. 38 | // If any headers are created - they will not be included in main.c 39 | // If this is needed - add file path to `Includes()` return value. 40 | WriteFiles() []WriteFile 41 | // ZephyrModules adds a list of modules that extender provides. 42 | // Module can be anything(I guess). 43 | // 44 | // Module name will also write all files in templates 45 | // from the `modules/` path for each provided module. 46 | // 47 | // https://docs.zephyrproject.org/latest/develop/modules.html 48 | ZephyrModules() []string 49 | } 50 | 51 | var _ Adder = SimpleExtender{} 52 | var _ Extender = SimpleExtender{} 53 | 54 | type SimpleExtender struct { 55 | // Name is unique identifier of extender. 56 | // Used only for deduplication purpuses. 57 | Name string 58 | IncludeHeaders []string 59 | TemplateName string 60 | FilesToWrite []WriteFile 61 | ZephyrModuleNames []string 62 | Config []appconfig.ConfigValue 63 | OverlayFn func(overlay *devicetree.DeviceTree) error 64 | } 65 | 66 | func ExtenderName(e Extender) string { 67 | extenderName := strings.TrimPrefix(fmt.Sprintf("%T", e), "*") 68 | if simpleExtender, ok := e.(SimpleExtender); ok { 69 | extenderName = simpleExtender.Name 70 | if extenderName == "" { 71 | panic("all simple extenders require `Name` field to be set") 72 | } 73 | } 74 | 75 | return extenderName 76 | } 77 | 78 | // Includes implements Extender. 79 | func (e SimpleExtender) Includes() []string { 80 | return e.IncludeHeaders 81 | } 82 | 83 | // Template implements Extender. 84 | func (e SimpleExtender) Template() string { 85 | return e.TemplateName 86 | } 87 | 88 | // WriteFiles implements Extender. 89 | func (e SimpleExtender) WriteFiles() []WriteFile { 90 | return e.FilesToWrite 91 | } 92 | 93 | func (e SimpleExtender) ZephyrModules() []string { 94 | return e.ZephyrModuleNames 95 | } 96 | 97 | func (e SimpleExtender) AppConfig() []appconfig.ConfigValue { 98 | return e.Config 99 | } 100 | 101 | func (e SimpleExtender) ApplyOverlay(overlay *devicetree.DeviceTree) error { 102 | if e.OverlayFn == nil { 103 | return nil 104 | } 105 | 106 | return e.OverlayFn(overlay) 107 | } 108 | -------------------------------------------------------------------------------- /types/option.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ffenix113/zigbee_home/types/yamlstrict" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | var _ yaml.Unmarshaler = (*Option[byte])(nil) 11 | 12 | type Option[T any] struct { 13 | hasValue bool 14 | value T 15 | } 16 | 17 | func NewOption[T any](v T) Option[T] { 18 | return Option[T]{ 19 | hasValue: true, 20 | value: v, 21 | } 22 | } 23 | 24 | func NewEmptyOption[T any]() Option[T] { 25 | return Option[T]{} 26 | } 27 | 28 | func (o *Option[T]) Set(v T) { 29 | o.hasValue = true 30 | o.value = v 31 | } 32 | 33 | func (o *Option[T]) Reset() { 34 | (*o) = Option[T]{} 35 | } 36 | 37 | func (o Option[T]) HasValue() bool { 38 | return o.hasValue 39 | } 40 | 41 | func (o Option[T]) Value() T { 42 | return o.value 43 | } 44 | 45 | func (o Option[T]) String() string { 46 | return fmt.Sprint(o.Value()) 47 | } 48 | 49 | // Format is a proxy for Option to format value using some verb. 50 | func (o Option[T]) Format(f fmt.State, verb rune) { 51 | fmt.Fprintf(f, fmt.FormatString(f, verb), o.Value()) 52 | } 53 | 54 | func (o *Option[T]) UnmarshalYAML(n *yaml.Node) error { 55 | var val T 56 | 57 | if err := yamlstrict.Unmarshal(&val, n); err != nil { 58 | return fmt.Errorf("unmarshal option with type %T: %w", val, err) 59 | } 60 | 61 | o.hasValue = true 62 | o.value = val 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /types/pin.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | "strconv" 8 | 9 | "github.com/ffenix113/zigbee_home/types/yamlstrict" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | var _ yaml.Unmarshaler = (*Pin)(nil) 14 | 15 | // PinWithID is similar to Pin, 16 | // but the difference is that user can define ID & short Pin for this type. 17 | // 18 | // So insted of `{id: 'some', port: 0, pin: 1}` 19 | // user can provide `{id: 'some', pin: 0.04}`. 20 | // 21 | // Still, probably there is a more elegant way to achieve this. 22 | type PinWithID struct { 23 | ID string 24 | Pin Pin 25 | } 26 | 27 | func (p PinWithID) ToPin() Pin { 28 | if p.ID != "" && p.Pin.ID != "" { 29 | // This log message can be improved 30 | log.Fatalf("cannot have id & pin.id set at the same time for pin %#v", p) 31 | } 32 | 33 | if p.Pin.ID == "" { 34 | p.Pin.ID = p.ID 35 | } 36 | 37 | return p.Pin 38 | } 39 | 40 | type PinWithIDSlice []PinWithID 41 | 42 | func (s PinWithIDSlice) ToPins() []Pin { 43 | pins := make([]Pin, 0, len(s)) 44 | 45 | for _, pinWithID := range s { 46 | pins = append(pins, pinWithID.ToPin()) 47 | } 48 | 49 | return pins 50 | } 51 | 52 | type Pin struct { 53 | ID string 54 | Port Option[uint8] 55 | Pin Option[uint8] 56 | Inverted bool 57 | } 58 | 59 | func (p Pin) Name() string { 60 | if p.ID != "" { 61 | return p.ID 62 | } 63 | 64 | return p.Label() 65 | } 66 | 67 | func (p Pin) Label() string { 68 | return fmt.Sprintf("pin%s", p.NumericLabel()) 69 | } 70 | 71 | func (p Pin) NumericLabel() string { 72 | return fmt.Sprintf("%d%d", p.Port, p.Pin) 73 | } 74 | 75 | func (p Pin) PinsDefined() bool { 76 | return p.Port.HasValue() && p.Pin.HasValue() 77 | } 78 | 79 | func (p Pin) Valid() bool { 80 | port := p.Port.Value() 81 | pin := p.Pin.Value() 82 | 83 | return p.ID != "" || 84 | (p.PinsDefined() && ((port == 0 && pin <= 31) || 85 | (port == 1 && pin <= 15))) 86 | } 87 | 88 | var pinRegex = regexp.MustCompile(`^([01])\.([0-3][0-9])$`) 89 | 90 | func (p *Pin) UnmarshalYAML(value *yaml.Node) error { 91 | if value.Kind != yaml.ScalarNode { 92 | // A trick to remove custom unmarshaling behavior. 93 | type pin Pin 94 | if err := yamlstrict.Unmarshal((*pin)(p), value); err != nil { 95 | return fmt.Errorf("unmarshal pin: %w", err) 96 | } 97 | 98 | if !p.Valid() { 99 | return fmt.Errorf("pin has invalid definition(no pins & no id)") 100 | } 101 | 102 | return nil 103 | } 104 | 105 | if value.Value == "" { 106 | return fmt.Errorf("pin definition cannot be empty") 107 | } 108 | 109 | matches := pinRegex.FindStringSubmatch(value.Value) 110 | if matches == nil { 111 | return fmt.Errorf("pin definition must be in a form of X.XX, where X is a number, but got: %q", value.Value) 112 | } 113 | 114 | port, err := strconv.ParseUint(matches[1], 10, 8) 115 | if err != nil { 116 | return fmt.Errorf("pin's port %q is invalid: %w", matches[1], err) 117 | } 118 | 119 | pin, err := strconv.ParseUint(matches[2], 10, 8) 120 | if err != nil { 121 | return fmt.Errorf("pin's pin %q is invalid: %w", matches[2], err) 122 | } 123 | 124 | p.Port = NewOption(uint8(port)) 125 | p.Pin = NewOption(uint8(pin)) 126 | 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /types/pin_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ffenix113/zigbee_home/types" 7 | "github.com/stretchr/testify/require" 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | func TestPin(t *testing.T) { 12 | t.Run("unmarshal yaml", func(t *testing.T) { 13 | type s struct { 14 | Pin types.Pin 15 | } 16 | 17 | tests := []struct { 18 | name string 19 | skipTest bool 20 | raw string 21 | unmarshaled types.Pin 22 | err bool 23 | }{ 24 | { 25 | name: "valid short", 26 | raw: `pin: 0.00`, 27 | unmarshaled: types.Pin{ 28 | Port: types.NewOption(uint8(0)), 29 | Pin: types.NewOption(uint8(0)), 30 | }, 31 | }, 32 | { 33 | // FIXME: Unmarshaler defined on Pin 34 | // will not be called for null yaml values. 35 | // This results in unmarshaling passing 36 | // and this test becoming worthless. 37 | // Assume to be non-critical issue, 38 | // and to be mitigated with configuration 39 | // validations. 40 | name: "invalid short, no pins", 41 | raw: `pin: null`, 42 | // Special case, to not assume that the test is passing. 43 | skipTest: true, 44 | }, 45 | { 46 | name: "invalid long, no pins", 47 | raw: `pin: {}`, 48 | err: true, 49 | }, 50 | { 51 | name: "valid long", 52 | raw: `pin: {port: 1, pin: 03, inverted: true}`, 53 | unmarshaled: types.Pin{ 54 | Port: types.NewOption(uint8(1)), 55 | Pin: types.NewOption(uint8(3)), 56 | Inverted: true, 57 | }, 58 | }, 59 | { 60 | name: "invalid long", 61 | raw: `pin: {port: 5, pin: 03, inverted: true}`, 62 | err: true, 63 | }, 64 | { 65 | name: "valid long, no pins", 66 | raw: `pin: {id: pin1}`, 67 | unmarshaled: types.Pin{ 68 | ID: "pin1", 69 | Port: types.NewEmptyOption[uint8](), 70 | Pin: types.NewEmptyOption[uint8](), 71 | }, 72 | }, 73 | } 74 | 75 | for _, test := range tests { 76 | test := test 77 | 78 | t.Run(test.name, func(t *testing.T) { 79 | if test.skipTest { 80 | t.SkipNow() 81 | } 82 | 83 | var s struct { 84 | Pin types.Pin 85 | } 86 | 87 | err := yaml.Unmarshal([]byte(test.raw), &s) 88 | if test.err { 89 | // TODO: We can check that error matches as well. 90 | require.Error(t, err) 91 | return 92 | } 93 | 94 | require.NoError(t, err) 95 | require.Equal(t, test.unmarshaled, s.Pin) 96 | }) 97 | } 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /types/semver.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | type Semver [3]uint8 10 | 11 | var versionRegx = regexp.MustCompile(`^v?(\d+)\.(\d+)(?:\.(\d+))?(?:-.*)?$`) 12 | 13 | func NewSemver(major, minor, patch uint8) Semver { 14 | return Semver{major, minor, patch} 15 | } 16 | 17 | func ParseSemver(ver string) (Semver, error) { 18 | match := versionRegx.FindStringSubmatch(ver) 19 | if match == nil { 20 | return Semver{}, fmt.Errorf("incorrect version %q", ver) 21 | } 22 | 23 | parsePart := func(part string) (uint8, error) { 24 | if part == "" { 25 | return 0, nil 26 | } 27 | 28 | parsed, err := strconv.ParseUint(part, 10, 8) 29 | if err != nil { 30 | return 0, fmt.Errorf("should not happen: bad part of the version: %q", part) 31 | } 32 | 33 | return uint8(parsed), nil 34 | } 35 | 36 | var parsed [3]uint8 37 | for i, part := range match[1:] { 38 | uintPart, err := parsePart(part) 39 | if err != nil { 40 | return Semver{}, fmt.Errorf("parse part %q: %w", part, err) 41 | } 42 | parsed[i] = uintPart 43 | } 44 | 45 | return Semver{parsed[0], parsed[1], parsed[2]}, nil 46 | } 47 | 48 | func (s Semver) String() string { 49 | return fmt.Sprintf("v%d.%d.%d", s[0], s[1], s[2]) 50 | } 51 | 52 | // SameMajorMinor checks if major and minor versions are equal. 53 | func (s Semver) SameMajorMinor(another Semver) bool { 54 | return s[0] == another[0] && s[1] == another[1] 55 | } 56 | 57 | // Compare returns -1 if receiver is smaller than another, 58 | // 1 if receiver is larger than another 59 | // and 0 if they are equal. 60 | func (s Semver) Compare(another Semver) int { 61 | if res := compare(s[0], another[0]); res != 0 { 62 | return res 63 | } 64 | 65 | if res := compare(s[1], another[1]); res != 0 { 66 | return res 67 | } 68 | 69 | return compare(s[2], another[2]) 70 | } 71 | 72 | func compare(a, b uint8) int { 73 | if a > b { 74 | return 1 75 | } 76 | 77 | if a < b { 78 | return -1 79 | } 80 | 81 | return 0 82 | } 83 | -------------------------------------------------------------------------------- /types/semver_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ffenix113/zigbee_home/types" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestSemverCompare(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | ver1, ver2 string 14 | result int 15 | }{ 16 | { 17 | name: "equal", 18 | ver1: "v1.1.1", 19 | ver2: "v1.1.1", 20 | result: 0, 21 | }, 22 | { 23 | name: "less", 24 | ver1: "v1.6.200", 25 | ver2: "v1.7.0", 26 | result: -1, 27 | }, 28 | { 29 | name: "greater", 30 | ver1: "v1.0.0", 31 | ver2: "v0.3", 32 | result: 1, 33 | }, 34 | } 35 | 36 | for _, test := range tests { 37 | t.Run(test.name, func(t *testing.T) { 38 | ver1, err := types.ParseSemver(test.ver1) 39 | require.NoError(t, err) 40 | 41 | ver2, err := types.ParseSemver(test.ver2) 42 | require.NoError(t, err) 43 | 44 | require.Equal(t, test.result, ver1.Compare(ver2)) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /types/sensor/known.go: -------------------------------------------------------------------------------- 1 | package sensor 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/ffenix113/zigbee_home/sensor" 8 | "github.com/ffenix113/zigbee_home/sensor/base" 9 | "github.com/ffenix113/zigbee_home/sensor/bosch" 10 | "github.com/ffenix113/zigbee_home/sensor/aosong" 11 | "github.com/ffenix113/zigbee_home/sensor/sensirion" 12 | ) 13 | 14 | func fromType[T Sensor]() Sensor { 15 | var s T 16 | 17 | rVal := reflect.New(reflect.TypeOf(s).Elem()) 18 | 19 | return rVal.Interface().(Sensor) 20 | } 21 | 22 | func fromConstructor(constr any) func() Sensor { 23 | return func() Sensor { 24 | rVal := reflect.ValueOf(constr) 25 | 26 | numOut := rVal.Type().NumOut() 27 | switch { 28 | case numOut == 0: 29 | panic("constructor must have 1 return value") 30 | case numOut > 1: 31 | retType := rVal.Type().Out(0) 32 | panic(fmt.Sprintf("constructor %q should return exactly 1 value", retType.String())) 33 | } 34 | 35 | ret := rVal.Call(nil)[0] 36 | 37 | return ret.Interface().(Sensor) 38 | } 39 | } 40 | 41 | var knownSensors = map[string]func() Sensor{ 42 | // Generic 43 | "on_off": fromType[*base.OnOff], 44 | "power_config": fromType[*base.PowerConfiguration], 45 | "contact": fromConstructor(base.NewContact), 46 | // Later we can just alias this to `soil_moisture` 47 | // if `soil_moisture` will not be used otherwise. 48 | "soil_moisture_adc": fromType[*base.SoilMoistureADC], 49 | // Generic ias zone sensor. 50 | // While it is defined here - for now it is 51 | // not useful much, as it only can be used 52 | // as contact sensor. 53 | "ias_zone": fromType[*base.IASZone], 54 | 55 | // Specific devices 56 | 57 | "device_temperature": fromType[*sensor.DeviceTemperature], 58 | 59 | // Bosch 60 | "bme280": fromConstructor(bosch.NewBME280), 61 | // This is a clone of bme280, with different overlay name 62 | // FIXME: It does not yet support IAQ measurements, 63 | // and does not expose resistance to Zigbee. 64 | "bme680": fromConstructor(bosch.NewBME680), 65 | 66 | // Aosong 67 | "dht": fromConstructor(aosong.NewDHT), 68 | 69 | // Sensirion 70 | "scd4x": fromType[*sensirion.SCD4X], 71 | } 72 | -------------------------------------------------------------------------------- /types/sensor/sensor.go: -------------------------------------------------------------------------------- 1 | package sensor 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/ffenix113/zigbee_home/sensor/base" 10 | "github.com/ffenix113/zigbee_home/types/appconfig" 11 | "github.com/ffenix113/zigbee_home/types/devicetree" 12 | "github.com/ffenix113/zigbee_home/types/generator" 13 | "github.com/ffenix113/zigbee_home/types/yamlstrict" 14 | "github.com/ffenix113/zigbee_home/zcl/cluster" 15 | "gopkg.in/yaml.v3" 16 | ) 17 | 18 | var sensorCounter int 19 | 20 | // SensorLabelFn returns a unique label for a sensor. 21 | var SensorLabelFn = func(s Sensor) string { 22 | sensorCounter += 1 23 | cleanLabel := strings.ReplaceAll(strings.ToLower(fmt.Sprintf("%T_", s)), ".", "_") 24 | return strings.TrimPrefix(cleanLabel, "*") + strconv.Itoa(sensorCounter) 25 | } 26 | 27 | type Sensors []Sensor 28 | 29 | type Sensor interface { 30 | // Stringer is for human-readable name 31 | fmt.Stringer 32 | // Label returns unique label value for the sensor. 33 | // Generally this method should not be defined by user, 34 | // intstead it will be defined in embedded `*base.Base` 35 | Label() string 36 | Template() string 37 | cluster.Provider 38 | appconfig.Provider 39 | devicetree.Applier 40 | } 41 | 42 | type WithExtenders interface { 43 | Extenders() []generator.Extender 44 | } 45 | 46 | // UniqueClusters will return all unique clusters across all the sensors. 47 | // Clusters might be configured for a specific sensor, 48 | // so this method is mostly useful to *know* which clusters are available. 49 | func (s *Sensors) UniqueClusters() cluster.Clusters { 50 | clusterMap := map[cluster.ID]struct{}{} 51 | 52 | var clusters cluster.Clusters 53 | 54 | for _, sensor := range *s { 55 | for _, cluster := range sensor.Clusters() { 56 | _, ok := clusterMap[cluster.ID()] 57 | if ok { 58 | continue 59 | } 60 | 61 | clusterMap[cluster.ID()] = struct{}{} 62 | clusters = append(clusters, cluster) 63 | } 64 | } 65 | 66 | return clusters 67 | } 68 | 69 | func (s *Sensors) UnmarshalYAML(value *yaml.Node) error { 70 | if value.Kind != yaml.SequenceNode { 71 | return fmt.Errorf("must have sequence, but have %q", value.Kind) 72 | } 73 | 74 | for i, node := range value.Content { 75 | sensor, err := unmarshalSensor(node) 76 | if err != nil { 77 | return fmt.Errorf("unmarshal sensor %d: %w", i, err) 78 | } 79 | 80 | (*s) = append((*s), sensor) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func unmarshalSensor(node *yaml.Node) (Sensor, error) { 87 | var sensorType base.SensorType 88 | if err := node.Decode(&sensorType); err != nil { //nolint:forbidigo // In this case we want specific subset of fields. 89 | return nil, fmt.Errorf("get sensor type: %w", err) 90 | } 91 | 92 | sensorConfigConstructor, ok := knownSensors[sensorType.Type] 93 | if !ok { 94 | return nil, fmt.Errorf("unsupported sensor type: %q", sensorType.Type) 95 | } 96 | 97 | rVal := reflect.ValueOf(sensorConfigConstructor()) 98 | if err := yamlstrict.Unmarshal(rVal.Interface(), node); err != nil { 99 | return nil, fmt.Errorf("decode sensor type %q: %w", sensorType.Type, err) 100 | } 101 | 102 | sensor := rVal.Interface().(Sensor) 103 | base := rVal.Elem().FieldByName("Base").Interface().(*base.Base) 104 | base.SetLabel(SensorLabelFn(sensor)) 105 | 106 | return sensor, nil 107 | } 108 | -------------------------------------------------------------------------------- /types/sensor/sensor_test.go: -------------------------------------------------------------------------------- 1 | package sensor_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ffenix113/zigbee_home/sensor/bosch" 7 | "github.com/ffenix113/zigbee_home/types/sensor" 8 | "github.com/stretchr/testify/require" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | func TestSensorsUnmarshal(t *testing.T) { 13 | bts := []byte(` 14 | - type: bme280 15 | connection: 16 | type: 'i2c1' 17 | address: '0x76'`) 18 | 19 | var sensors sensor.Sensors 20 | 21 | require.NoError(t, yaml.Unmarshal(bts, &sensors)) 22 | 23 | require.Equal(t, "bme280", sensors[0].(*bosch.BME280).Base.Type) 24 | require.Equal(t, "0x76", sensors[0].(*bosch.BME280).Connection["address"]) 25 | } 26 | -------------------------------------------------------------------------------- /types/sensor/simple.go: -------------------------------------------------------------------------------- 1 | package sensor 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/types/appconfig" 5 | "github.com/ffenix113/zigbee_home/types/devicetree" 6 | "github.com/ffenix113/zigbee_home/zcl/cluster" 7 | ) 8 | 9 | var _ Sensor = (*Simple)(nil) 10 | 11 | type Simple struct { 12 | SensorName string 13 | SensorLabel string 14 | SensorTemplate string 15 | SensorClusters cluster.Clusters 16 | SensorAppConfig []appconfig.ConfigValue 17 | SensorAppOverlay func(*devicetree.DeviceTree) error 18 | } 19 | 20 | func (s *Simple) String() string { 21 | return s.SensorName 22 | } 23 | 24 | func (s *Simple) Label() string { 25 | return s.SensorLabel 26 | } 27 | 28 | func (s *Simple) Template() string { 29 | return s.SensorTemplate 30 | } 31 | 32 | func (s *Simple) Clusters() cluster.Clusters { 33 | return s.SensorClusters 34 | } 35 | 36 | func (s *Simple) AppConfig() []appconfig.ConfigValue { 37 | return s.SensorAppConfig 38 | } 39 | 40 | func (s *Simple) ApplyOverlay(overlay *devicetree.DeviceTree) error { 41 | return s.SensorAppOverlay(overlay) 42 | } 43 | -------------------------------------------------------------------------------- /types/source/source.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "github.com/ffenix113/zigbee_home/config" 5 | "github.com/ffenix113/zigbee_home/templates" 6 | "github.com/ffenix113/zigbee_home/types" 7 | "github.com/ffenix113/zigbee_home/types/generator" 8 | ) 9 | 10 | type Source struct { 11 | templates *templates.Templates 12 | } 13 | 14 | func NewSource(ncsVersion types.Semver) *Source { 15 | return &Source{ 16 | templates: templates.NewTemplates(templates.TemplateFS, ncsVersion), 17 | } 18 | } 19 | 20 | func (s *Source) WriteTo(srcDir string, device *config.Device, extenders []generator.Extender) error { 21 | return s.templates.WriteTo(srcDir, device, extenders) 22 | } 23 | -------------------------------------------------------------------------------- /types/yamlstrict/unmarshal.go: -------------------------------------------------------------------------------- 1 | package yamlstrict 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | func Unmarshal(to any, node *yaml.Node) error { 11 | marshaled, err := yaml.Marshal(node) 12 | if err != nil { 13 | return fmt.Errorf("marshaling node to bytes: %w", err) 14 | } 15 | 16 | dec := yaml.NewDecoder(bytes.NewReader(marshaled)) 17 | dec.KnownFields(true) 18 | 19 | err = dec.Decode(to) 20 | if err != nil { 21 | return fmt.Errorf("unmarshal strict: %w", err) 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /types/zigbee.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/ffenix113/zigbee_home/zcl/cluster" 4 | 5 | type Endpoints []*Endpoint 6 | 7 | type Endpoint struct { 8 | Num int 9 | Clusters cluster.Clusters 10 | } 11 | 12 | func (e *Endpoints) LastEndpoint() *Endpoint { 13 | if len(*e) == 0 { 14 | return e.AddEndpoint() 15 | } 16 | 17 | return (*e)[len(*e)-1] 18 | } 19 | 20 | func (e *Endpoints) AddEndpoint() *Endpoint { 21 | // This will create first endpoint with number 1 22 | *e = append(*e, &Endpoint{Num: len(*e) + 1}) 23 | return (*e)[len(*e)-1] 24 | } 25 | 26 | func (e *Endpoint) HasCluster(clusterID cluster.ID) bool { 27 | for _, cluster := range e.Clusters { 28 | if cluster.ID() == clusterID { 29 | return true 30 | } 31 | } 32 | 33 | return false 34 | } 35 | 36 | func (e *Endpoint) AddCluster(cluster cluster.Cluster) { 37 | e.Clusters = append(e.Clusters, cluster) 38 | } 39 | -------------------------------------------------------------------------------- /zboss_license.txt: -------------------------------------------------------------------------------- 1 | ZBOSS Zigbee 3.0 2 | 3 | Copyright (c) 2012-2022 DSR Corporation, Denver CO, USA. 4 | www.dsr-zboss.com 5 | www.dsr-corporation.com 6 | All rights reserved. 7 | 8 | 9 | Use in source and binary forms, redistribution in binary form only, with 10 | or without modification, are permitted provided that the following conditions 11 | are met: 12 | 13 | 1. Redistributions in binary form, except as embedded into a Nordic 14 | Semiconductor ASA integrated circuit in a product or a software update for 15 | such product, must reproduce the above copyright notice, this list of 16 | conditions and the following disclaimer in the documentation and/or other 17 | materials provided with the distribution. 18 | 19 | 2. Neither the name of Nordic Semiconductor ASA nor the names of its 20 | contributors may be used to endorse or promote products derived from this 21 | software without specific prior written permission. 22 | 23 | 3. This software, with or without modification, must only be used with a Nordic 24 | Semiconductor ASA integrated circuit. 25 | 26 | 4. Any software provided in binary form under this license must not be reverse 27 | engineered, decompiled, modified and/or disassembled. 28 | 29 | THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS OR 30 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 31 | MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE 32 | DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE LIABLE 33 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 34 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 35 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 36 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 37 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 38 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | -------------------------------------------------------------------------------- /zcl/cluster/basic.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | var _ Cluster = Basic{} 4 | 5 | // ZCL 3.5 6 | type Basic struct{} 7 | 8 | func (t Basic) ID() ID { 9 | return ID_BASIC 10 | } 11 | 12 | func (Basic) CAttrType() string { 13 | return "zb_zcl_basic_attrs_t" 14 | } 15 | func (Basic) CVarName() string { 16 | return "basic" 17 | } 18 | 19 | func (Basic) ReportAttrCount() int { 20 | return 0 21 | } 22 | 23 | func (Basic) Side() Side { 24 | return Server 25 | } 26 | -------------------------------------------------------------------------------- /zcl/cluster/carbon_dioxide.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | var _ Cluster = CarbonDioxide{} 4 | 5 | // ZCL 4.5.2 6 | type CarbonDioxide struct { 7 | MinMeasuredValue int16 8 | MaxMeasuredValue int16 9 | Tolerance uint16 10 | } 11 | 12 | func (t CarbonDioxide) ID() ID { 13 | return ID_CARBON_DIOXIDE 14 | } 15 | 16 | func (CarbonDioxide) CAttrType() string { 17 | return "zb_zcl_measurement_type_single_attrs_t" 18 | } 19 | func (CarbonDioxide) CVarName() string { 20 | return "carbon_dioxide" 21 | } 22 | 23 | func (CarbonDioxide) ReportAttrCount() int { 24 | return 1 25 | } 26 | 27 | func (CarbonDioxide) Side() Side { 28 | return Server 29 | } 30 | -------------------------------------------------------------------------------- /zcl/cluster/cluster_ids.go: -------------------------------------------------------------------------------- 1 | // Parts of this file can come from ZBOSS nRF SDK v2.5.0: 2 | // /v2.5.0/nrfxlib/zboss/production/include/zcl/zb_zcl_common.h 3 | /* 4 | * ZBOSS Zigbee 3.0 5 | * 6 | * Copyright (c) 2012-2022 DSR Corporation, Denver CO, USA. 7 | * www.dsr-zboss.com 8 | * www.dsr-corporation.com 9 | * All rights reserved. 10 | * 11 | * 12 | * Use in source and binary forms, redistribution in binary form only, with 13 | * or without modification, are permitted provided that the following conditions 14 | * are met: 15 | * 16 | * 1. Redistributions in binary form, except as embedded into a Nordic 17 | * Semiconductor ASA integrated circuit in a product or a software update for 18 | * such product, must reproduce the above copyright notice, this list of 19 | * conditions and the following disclaimer in the documentation and/or other 20 | * materials provided with the distribution. 21 | * 22 | * 2. Neither the name of Nordic Semiconductor ASA nor the names of its 23 | * contributors may be used to endorse or promote products derived from this 24 | * software without specific prior written permission. 25 | * 26 | * 3. This software, with or without modification, must only be used with a Nordic 27 | * Semiconductor ASA integrated circuit. 28 | * 29 | * 4. Any software provided in binary form under this license must not be reverse 30 | * engineered, decompiled, modified and/or disassembled. 31 | * 32 | * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS OR 33 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 34 | * MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE 35 | * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE LIABLE 36 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 37 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 38 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 39 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 40 | * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 41 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 42 | */ 43 | 44 | package cluster 45 | 46 | import ( 47 | "fmt" 48 | "strings" 49 | ) 50 | 51 | type ID int 52 | 53 | func (id ID) ToName() (string, error) { 54 | var value string 55 | 56 | switch id { 57 | case ID_BASIC: 58 | value = "basic" 59 | case ID_POWER_CONFIG: 60 | value = "power_config" 61 | case ID_DEVICE_TEMP_CONFIG: 62 | value = "device_temp_config" 63 | case ID_IDENTIFY: 64 | value = "identify" 65 | case ID_ON_OFF: 66 | value = "on_off" 67 | case ID_TEMP_MEASUREMENT: 68 | value = "temp_measurement" 69 | case ID_PRESSURE_MEASUREMENT: 70 | value = "pressure_measurement" 71 | case ID_REL_HUMIDITY_MEASUREMENT: 72 | value = "rel_humidity_measurement" 73 | case ID_CARBON_DIOXIDE: 74 | value = "carbon_dioxide" 75 | case ID_IAS_ZONE: 76 | value = "ias_zone" 77 | case ID_SOIL_MOISTURE_MEASUREMENT: 78 | value = "soil_moisture" 79 | } 80 | 81 | if value == "" { 82 | return "", fmt.Errorf("cluster ID %d does not have string represenstation", id) 83 | } 84 | 85 | return value, nil 86 | } 87 | 88 | func (id ID) ToZCL() (string, error) { 89 | clusterName, err := id.ToName() 90 | if err != nil { 91 | return "", err 92 | } 93 | 94 | return "ZB_ZCL_CLUSTER_ID_" + strings.ToUpper(clusterName), nil 95 | } 96 | 97 | const ID_BASIC ID = 0 // Basic cluster identifier. 98 | const ID_DEVICE_TEMP_CONFIG ID = 2 // Device temperature cluster. 99 | const ID_IDENTIFY ID = 3 // Identify cluster identifier. 100 | const ID_ON_OFF ID = 6 // On/Off cluster identifier. 101 | const ID_POWER_CONFIG ID = 1 102 | 103 | /* Measurement and Sensing */ 104 | const ID_TEMP_MEASUREMENT ID = 0x0402 // Temperature measurement 105 | const ID_PRESSURE_MEASUREMENT ID = 0x0403 // Pressure measurement 106 | const ID_REL_HUMIDITY_MEASUREMENT ID = 0x0405 // Relative humidity measurement 107 | const ID_SOIL_MOISTURE_MEASUREMENT ID = 0x0408 // Soil moisture measurement 108 | const ID_CARBON_DIOXIDE ID = 0x040d 109 | 110 | const ID_IAS_ZONE ID = 0x0500 111 | -------------------------------------------------------------------------------- /zcl/cluster/common.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | const ( 10 | None Side = iota 11 | Server Side = 1 << iota 12 | Client Side = 1 << iota 13 | ClientAndServer Side = Server | Client 14 | ) 15 | 16 | var _ yaml.Unmarshaler = (*Side)(nil) 17 | 18 | type Side uint8 19 | 20 | type Provider interface { 21 | Clusters() Clusters 22 | } 23 | 24 | type Cluster interface { 25 | ID() ID 26 | CAttrType() string 27 | CVarName() string 28 | // ReportAttrCount returns how many attributes are reportable 29 | ReportAttrCount() int 30 | // Side tells if the cluster is client and/or server. ZCL 1.3. 31 | Side() Side 32 | } 33 | 34 | type Clusters []Cluster 35 | 36 | func (s Side) String() string { 37 | switch s { 38 | case Client: 39 | return "ZB_ZCL_CLUSTER_CLIENT_ROLE" 40 | case Server: 41 | return "ZB_ZCL_CLUSTER_SERVER_ROLE" 42 | default: 43 | return "" // Idea is that build will break on invalid value. 44 | } 45 | } 46 | 47 | func (s Side) IsClient() bool { 48 | return s == Client 49 | } 50 | 51 | func (s Side) IsServer() bool { 52 | return s == Server 53 | } 54 | 55 | func (s *Side) UnmarshalYAML(node *yaml.Node) error { 56 | switch node.Value { 57 | case "server": 58 | *s = Server 59 | case "client": 60 | *s = Client 61 | case "client_and_server": 62 | *s = ClientAndServer 63 | default: 64 | return fmt.Errorf("unknown side value: %q", node.Value) 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func (c Clusters) ReportAttrCount() (count int) { 71 | for _, cluster := range c { 72 | count += cluster.ReportAttrCount() 73 | } 74 | 75 | return count 76 | } 77 | 78 | func (c Clusters) Servers() (count int) { 79 | for _, cluster := range c { 80 | if cluster.Side()&Server == Server { 81 | count++ 82 | } 83 | } 84 | 85 | return count 86 | } 87 | 88 | func (c Clusters) Clients() (count int) { 89 | for _, cluster := range c { 90 | if cluster.Side()&Client == Client { 91 | count++ 92 | } 93 | } 94 | 95 | return count 96 | } 97 | -------------------------------------------------------------------------------- /zcl/cluster/device_temperature.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | var _ Cluster = DeviceTemperature{} 4 | 5 | // ZCL 3.4.2 6 | type DeviceTemperature struct{} 7 | 8 | func (t DeviceTemperature) ID() ID { 9 | return ID_DEVICE_TEMP_CONFIG 10 | } 11 | 12 | func (DeviceTemperature) CAttrType() string { 13 | return "zb_zcl_device_temperature_config_attrs_t" 14 | } 15 | func (DeviceTemperature) CVarName() string { 16 | return "device_temperature" 17 | } 18 | 19 | func (DeviceTemperature) ReportAttrCount() int { 20 | return 0 21 | } 22 | 23 | func (DeviceTemperature) Side() Side { 24 | return Server 25 | } 26 | -------------------------------------------------------------------------------- /zcl/cluster/ias_zone.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | var _ Cluster = IASZone{} 4 | 5 | type IasZoneType string 6 | 7 | const ( 8 | IasZoneContact IasZoneType = "contact" 9 | ) 10 | 11 | func (c IasZoneType) String() string { 12 | var suffix string 13 | 14 | switch c { 15 | case IasZoneContact: 16 | suffix = "CONTACT_SWITCH" 17 | default: 18 | // For now this would work 19 | suffix = "STANDARD_CIE" 20 | } 21 | 22 | return "ZB_ZCL_IAS_ZONE_ZONETYPE_" + suffix 23 | } 24 | 25 | // ZCL 4.5.2 26 | // 27 | // Currently only supports contact zone type. 28 | type IASZone struct { 29 | ZoneType IasZoneType 30 | } 31 | 32 | func (z IASZone) ID() ID { 33 | return ID_IAS_ZONE 34 | } 35 | 36 | func (IASZone) CAttrType() string { 37 | return "zb_zcl_ias_zone_attrs_t" 38 | } 39 | func (IASZone) CVarName() string { 40 | return "ias_zone" 41 | } 42 | 43 | func (IASZone) ReportAttrCount() int { 44 | return 0 45 | } 46 | 47 | func (z IASZone) Side() Side { 48 | return Server 49 | } 50 | -------------------------------------------------------------------------------- /zcl/cluster/identify.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | var _ Cluster = Identify{} 4 | 5 | // ZCL 3.5 6 | type Identify struct{} 7 | 8 | func (t Identify) ID() ID { 9 | return ID_IDENTIFY 10 | } 11 | 12 | func (Identify) CAttrType() string { 13 | return "zb_zcl_identify_attrs_t" 14 | } 15 | func (Identify) CVarName() string { 16 | return "identify" 17 | } 18 | 19 | func (Identify) ReportAttrCount() int { 20 | return 0 21 | } 22 | 23 | func (Identify) Side() Side { 24 | // Can be added as client as well, 25 | // when supported by templates. 26 | return Server 27 | } 28 | -------------------------------------------------------------------------------- /zcl/cluster/on_off.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | type OnOff struct { 4 | PinLabel string 5 | } 6 | 7 | func (o OnOff) ID() ID { 8 | return ID_ON_OFF 9 | } 10 | 11 | func (OnOff) CAttrType() string { 12 | return "zb_zcl_on_off_attrs_t" 13 | } 14 | func (OnOff) CVarName() string { 15 | return "on_off" 16 | } 17 | 18 | func (OnOff) ReportAttrCount() int { 19 | return 1 20 | } 21 | 22 | func (OnOff) Side() Side { 23 | // This only allows to be controlled from the client, 24 | // i.e. home assistant. 25 | // But it cannot be controlled from device for now. 26 | return Server 27 | } 28 | -------------------------------------------------------------------------------- /zcl/cluster/power_config.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | type PowerConfiguration struct { 4 | BatteryRatedVoltage uint16 `yaml:"battery_rated_voltage"` 5 | BatteryVoltageMinThreshold uint16 `yaml:"battery_voltage_min_threshold"` 6 | } 7 | 8 | func (o PowerConfiguration) ID() ID { 9 | return ID_POWER_CONFIG 10 | } 11 | 12 | func (PowerConfiguration) CAttrType() string { 13 | return "zb_zcl_power_config_attrs_t" 14 | } 15 | func (PowerConfiguration) CVarName() string { 16 | return "power_config" 17 | } 18 | 19 | func (PowerConfiguration) ReportAttrCount() int { 20 | return 1 21 | } 22 | 23 | func (PowerConfiguration) Side() Side { 24 | return Server 25 | } 26 | -------------------------------------------------------------------------------- /zcl/cluster/pressure.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | var _ Cluster = Pressure{} 4 | 5 | // ZCL 4.5.2 6 | type Pressure struct { 7 | MinMeasuredValue int16 8 | MaxMeasuredValue int16 9 | Tolerance uint16 10 | } 11 | 12 | func (t Pressure) ID() ID { 13 | return ID_PRESSURE_MEASUREMENT 14 | } 15 | 16 | func (Pressure) CAttrType() string { 17 | return "zb_zcl_pressure_measurement_attrs_t" 18 | } 19 | func (Pressure) CVarName() string { 20 | return "pressure" 21 | } 22 | 23 | func (Pressure) ReportAttrCount() int { 24 | return 1 25 | } 26 | 27 | func (Pressure) Side() Side { 28 | return Server 29 | } 30 | -------------------------------------------------------------------------------- /zcl/cluster/temperature.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | var _ Cluster = Temperature{} 4 | 5 | // ZCL 4.4.2 6 | type Temperature struct { 7 | MinMeasuredValue int16 8 | MaxMeasuredValue int16 9 | Tolerance uint16 10 | } 11 | 12 | func (t Temperature) ID() ID { 13 | return ID_TEMP_MEASUREMENT 14 | } 15 | 16 | func (Temperature) CAttrType() string { 17 | return "zb_zcl_temp_measurement_attrs_t" 18 | } 19 | func (Temperature) CVarName() string { 20 | return "temperature" 21 | } 22 | 23 | func (Temperature) ReportAttrCount() int { 24 | return 1 25 | } 26 | 27 | func (Temperature) Side() Side { 28 | return Server 29 | } 30 | -------------------------------------------------------------------------------- /zcl/cluster/water_content.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | var _ Cluster = WaterContent{} 4 | 5 | func NewRelativeHumidity(minVal, maxVal uint16) *WaterContent { 6 | return &WaterContent{ 7 | MinMeasuredValue: minVal, 8 | MaxMeasuredValue: maxVal, 9 | 10 | ClusterID: ID_REL_HUMIDITY_MEASUREMENT, 11 | CVarNameStr: "humidity", 12 | } 13 | } 14 | 15 | func NewSoilMoisture(minVal, maxVal uint16) *WaterContent { 16 | return &WaterContent{ 17 | MinMeasuredValue: minVal, 18 | MaxMeasuredValue: maxVal, 19 | 20 | ClusterID: ID_SOIL_MOISTURE_MEASUREMENT, 21 | CVarNameStr: "soil_moisture", 22 | } 23 | } 24 | 25 | // ZCL 4.7.2 26 | type WaterContent struct { 27 | MinMeasuredValue uint16 28 | MaxMeasuredValue uint16 29 | // Tolerance is not supported for humidity in nRF Connect SDK v2.5.0 30 | // Tolerance uint16 31 | 32 | ClusterID ID `yaml:"-"` 33 | CVarNameStr string `yaml:"-"` 34 | } 35 | 36 | func (wc WaterContent) ID() ID { 37 | return wc.ClusterID 38 | } 39 | 40 | func (wc WaterContent) CAttrType() string { 41 | return "zb_zcl_water_content_attrs_t" 42 | } 43 | func (wc WaterContent) CVarName() string { 44 | return wc.CVarNameStr 45 | } 46 | 47 | func (WaterContent) ReportAttrCount() int { 48 | return 1 49 | } 50 | 51 | func (WaterContent) Side() Side { 52 | return Server 53 | } 54 | --------------------------------------------------------------------------------