From 74ca4e04bb521c2594379ed2b674f1771395abc5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1bor=20Poczkodi?= <gabest11@gmail.com>
Date: Fri, 11 Oct 2024 17:25:41 +0200
Subject: [PATCH] KT0803 FM Transmitter initial commit

---
 CODEOWNERS                                    |   1 +
 esphome/components/kt0803/__init__.py         |  86 ++++
 esphome/components/kt0803/kt0803.cpp          | 283 +++++++++++++
 esphome/components/kt0803/kt0803.h            |  74 ++++
 esphome/components/kt0803/kt0803defs.h        | 398 ++++++++++++++++++
 esphome/components/kt0803/number/__init__.py  |  38 ++
 .../kt0803/number/frequency_number.cpp        |  12 +
 .../kt0803/number/frequency_number.h          |  18 +
 8 files changed, 910 insertions(+)
 create mode 100644 esphome/components/kt0803/__init__.py
 create mode 100644 esphome/components/kt0803/kt0803.cpp
 create mode 100644 esphome/components/kt0803/kt0803.h
 create mode 100644 esphome/components/kt0803/kt0803defs.h
 create mode 100644 esphome/components/kt0803/number/__init__.py
 create mode 100644 esphome/components/kt0803/number/frequency_number.cpp
 create mode 100644 esphome/components/kt0803/number/frequency_number.h

diff --git a/CODEOWNERS b/CODEOWNERS
index ed9c13a975..8faa3526ea 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -219,6 +219,7 @@ esphome/components/json/* @OttoWinter
 esphome/components/kamstrup_kmp/* @cfeenstra1024
 esphome/components/key_collector/* @ssieb
 esphome/components/key_provider/* @ssieb
+esphome/components/kt0803/* @gabest11
 esphome/components/kuntze/* @ssieb
 esphome/components/lcd_menu/* @numo68
 esphome/components/ld2410/* @regevbr @sebcaps
diff --git a/esphome/components/kt0803/__init__.py b/esphome/components/kt0803/__init__.py
new file mode 100644
index 0000000000..77a2a214c2
--- /dev/null
+++ b/esphome/components/kt0803/__init__.py
@@ -0,0 +1,86 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import automation
+from esphome.components import i2c, sensor, text_sensor, binary_sensor
+from esphome.const import (
+    CONF_ID,
+    CONF_FREQUENCY,
+    DEVICE_CLASS_POWER,
+    DEVICE_CLASS_EMPTY,
+)
+
+CODEOWNERS = ["@gabest11"]
+DEPENDENCIES = ["i2c"]
+AUTO_LOAD = ["sensor", "text_sensor", "binary_sensor", "number", "switch", "select", "text"]
+MULTI_CONF = True
+
+UNIT_MEGA_HERTZ = "MHz"
+UNIT_KILO_HERTZ = "KHz"
+UNIT_MILLI_VOLT = "mV"
+UNIT_MICRO_AMPERE = "mA"
+UNIT_DECIBEL_MICRO_VOLT = "dBµV"
+
+ICON_VOLUME_MUTE = "mdi:volume-mute"
+ICON_EAR_HEARING = "mdi:ear-hearing"
+ICON_RADIO_TOWER = "mdi:radio-tower"
+ICON_SLEEP = "mdi:sleep"
+ICON_SINE_WAVE = "mdi:sine-wave"
+ICON_RESISTOR = "mdi:resistor"
+ICON_FORMAT_TEXT = "mdi:format-text"
+
+kt0803_ns = cg.esphome_ns.namespace("kt0803")
+KT0803Component = kt0803_ns.class_(
+    "KT0803Component", cg.PollingComponent, i2c.I2CDevice
+)
+
+CONF_KT0803_ID = "kt0803_id"
+CONF_CHIP_ID = "chip_id"
+CONF_PW_OK = "pw_ok"
+CONF_SLNCID = "slncid"
+
+SetFrequencyAction = kt0803_ns.class_(
+    "SetFrequencyAction", automation.Action, cg.Parented.template(KT0803Component)
+)
+
+ChipId = kt0803_ns.enum("ChipId", True)
+CHIP_ID = {
+    "KT0803": ChipId.KT0803,
+    "KT0803K": ChipId.KT0803K,
+    "KT0803M": ChipId.KT0803M,
+    "KT0803L": ChipId.KT0803L,
+}
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(KT0803Component),
+            cv.Required(CONF_CHIP_ID): cv.enum(CHIP_ID),
+            cv.Optional(CONF_FREQUENCY, default=87.50): cv.float_range(70, 108),
+            cv.Optional(CONF_PW_OK): binary_sensor.binary_sensor_schema(
+                device_class=DEVICE_CLASS_POWER,
+                icon=ICON_RADIO_TOWER,
+            ),
+            cv.Optional(CONF_SLNCID): binary_sensor.binary_sensor_schema(
+                device_class=DEVICE_CLASS_EMPTY,
+                icon=ICON_VOLUME_MUTE,
+            ),
+        }
+    )
+    .extend(cv.polling_component_schema("60s"))
+    .extend(i2c.i2c_device_schema(0x3E))
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+    cg.add(var.set_chip_id(config.get(CONF_CHIP_ID)))
+    if conf_frequency := config.get(CONF_FREQUENCY):
+        cg.add(var.set_frequency(conf_frequency))
+    if conf_pw_ok := config.get(CONF_PW_OK):
+        s = await binary_sensor.new_binary_sensor(conf_pw_ok)
+        cg.add(var.set_pw_ok_binary_sensor(s))
+    if conf_slncid := config.get(CONF_SLNCID):
+        s = await binary_sensor.new_binary_sensor(conf_slncid)
+        cg.add(var.set_slncid_binary_sensor(s))
diff --git a/esphome/components/kt0803/kt0803.cpp b/esphome/components/kt0803/kt0803.cpp
new file mode 100644
index 0000000000..eaca52f95d
--- /dev/null
+++ b/esphome/components/kt0803/kt0803.cpp
@@ -0,0 +1,283 @@
+#include "kt0803.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+#include <cstdio>
+#include <cmath>
+
+namespace esphome {
+namespace kt0803 {
+
+// TODO: std::clamp isn't here yet
+#define clamp(v, lo, hi) std::max(std::min(v, hi), lo)
+
+static const char *const TAG = "kt0803";
+
+KT0803Component::KT0803Component() {
+  this->chip_id_ = ChipId::KT0803L;
+  this->reset_ = false;
+  memset(&this->state_, 0, sizeof(this->state_));
+  // set datasheet defaults, except frequency to 87.5MHz
+  this->state_.REG_00 = 0x6B;
+  this->state_.REG_01 = 0xC3;
+  this->state_.REG_02 = 0x40;
+  this->state_.REG_04 = 0x04;
+  this->state_.REG_0E = 0x02;
+  this->state_.REG_10 = 0xA8;
+  this->state_.REG_12 = 0x80;
+  this->state_.REG_13 = 0x80;
+  this->state_.REG_15 = 0xE0;
+  this->state_.REG_26 = 0xA0;
+}
+
+bool KT0803Component::check_reg_(uint8_t addr) {
+  switch (addr) {  // check KT0803 address range
+    case 0x00:
+    case 0x01:
+    case 0x02:
+    case 0x13:
+      return true;
+  }
+
+  if (this->chip_id_ != ChipId::KT0803) {
+    switch (addr) {  // check KT0803K/M address range too
+      case 0x00:
+      case 0x01:
+      case 0x02:
+      case 0x04:
+      case 0x0B:
+      case 0x0E:
+      case 0x0F:
+      case 0x10:
+      case 0x12:
+      case 0x14:
+      case 0x16:
+        return true;
+    }
+  }
+
+  if (this->chip_id_ == ChipId::KT0803L) {
+    switch (addr) {  // check KT0803L address range too
+      case 0x17:
+      case 0x1E:
+      case 0x26:
+      case 0x27:
+        return true;
+    }
+  }
+
+  return false;
+}
+
+void KT0803Component::write_reg_(uint8_t addr) {
+  if (addr >= sizeof(this->regs_) || !this->check_reg_(addr)) {
+    ESP_LOGE(TAG, "write_reg_(0x%02X) invalid register address", addr);
+    return;
+  }
+
+  if (addr == 0x13 && this->state_.PA_CTRL == 1) {
+    ESP_LOGW(TAG, "write_reg_(0x%02X) PA_CTRL = 1 can destroy the device", addr);
+    return; // TODO: remove this when everything tested and works
+  }
+
+  if (this->reset_) {
+    uint8_t value = this->regs_[addr];
+    ESP_LOGV(TAG, "write_reg_(0x%02X) = 0x%02X", addr, value);
+    this->write_byte(addr, value);
+  } else {
+    if (this->get_component_state() & COMPONENT_STATE_LOOP) {
+      ESP_LOGE(TAG, "write_reg_(0x%02X) device was not reset", addr);
+    }
+  }
+}
+
+bool KT0803Component::read_reg_(uint8_t addr) {
+  if (addr >= sizeof(this->regs_) || !this->check_reg_(addr)) {
+    ESP_LOGE(TAG, "write_reg_(0x%02X) invalid register address", addr);
+    return false;
+  }
+
+  uint8_t c;
+  if (i2c::ERROR_OK == this->read_register(addr, &c, 1, false)) {
+    this->regs_[addr] = c;
+    return true;
+  }
+
+  ESP_LOGE(TAG, "read_reg_(0x%02X) cannot read register", addr);
+  return false;
+}
+
+// overrides
+
+void KT0803Component::setup() {
+  /*
+  for (size_t addr = 0; addr < 0x2F; addr++) {
+    uint8_t c;
+    if (i2c::ERROR_OK == this->read_register(addr, &c, 1, false)) {
+      ESP_LOGV(TAG, "setup register[%02X]: %02X", addr, c);
+    }
+  }
+  */
+  
+  this->reset_ = true;
+
+  for (size_t addr = 0; addr < sizeof(this->state_); addr++) {
+    if (addr != 0x0F && this->check_reg_(addr)) {
+      this->write_reg_(addr);
+    }
+  }
+
+  this->publish_pw_ok();
+  this->publish_slncid();
+  this->publish_frequency();
+}
+
+void KT0803Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "KT0803:");
+  LOG_I2C_DEVICE(this);
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "failed!");
+  }
+  ESP_LOGCONFIG(TAG, "  Chip: %s", this->get_chip_string().c_str());
+  ESP_LOGCONFIG(TAG, "  Frequency: %.2f MHz", this->get_frequency());
+  // TODO: ...and everything else...
+  LOG_UPDATE_INTERVAL(this);
+}
+
+void KT0803Component::update() {
+  if (this->read_reg_(0x0F)) {
+    this->publish_pw_ok();
+    this->publish_slncid();
+  }
+/*
+  for (size_t addr = 0; addr < 0x2F; addr++) {
+    uint8_t c;
+    if (i2c::ERROR_OK == this->read_register(addr, &c, 1, false)) {
+      ESP_LOGV(TAG, "update register[%02X]: %02X", addr, c);
+    }
+  }
+*/
+}
+
+void KT0803Component::loop() {
+}
+
+// config
+
+void KT0803Component::set_chip_id(ChipId value) {
+  this->chip_id_ = value;
+}
+
+ChipId KT0803Component::get_chip_id() {
+  return this->chip_id_;
+}
+
+std::string KT0803Component::get_chip_string() const {
+  switch (this->chip_id_) {
+    case ChipId::KT0803:
+      return "KT0803";
+    case ChipId::KT0803K:
+      return "KT0803K";
+    case ChipId::KT0803M:
+      return "KT0803M";
+    case ChipId::KT0803L:
+      return "KT0803L";
+    default:
+      return "Unknown";
+  }
+}
+
+void KT0803Component::set_frequency(float value) {
+  if (!(CHSEL_MIN <= value && value <= CHSEL_MAX)) {
+    ESP_LOGE(TAG, "set_frequency(%.2f) invalid (%.2f - %.2f)", value, CHSEL_MIN, CHSEL_MAX);
+    return;
+  }
+
+  uint16_t ch = (uint16_t) std::lround(value * 20);
+  this->state_.CHSEL2 = (uint8_t) ((ch >> 9) & 0x07);
+  this->state_.CHSEL1 = (uint8_t) ((ch >> 1) & 0xff);
+  this->state_.CHSEL0 = (uint8_t) ((ch >> 0) & 0x01);
+  this->write_reg_(0x00);
+  this->write_reg_(0x01);
+  this->write_reg_(0x02);
+
+  this->publish_frequency();
+}
+
+float KT0803Component::get_frequency() {
+  uint16_t ch = 0;
+  ch |= (uint16_t) this->state_.CHSEL2 << 9;
+  ch |= (uint16_t) this->state_.CHSEL1 << 1;
+  if (this->chip_id_ != ChipId::KT0803) {
+    ch |= (uint16_t) this->state_.CHSEL0 << 0;
+  }
+  return (float) ch / 20;
+}
+
+// publish
+
+void KT0803Component::publish_pw_ok() { this->publish(this->pw_ok_binary_sensor_, this->state_.PW_OK == 1); }
+
+void KT0803Component::publish_slncid() { this->publish(this->slncid_binary_sensor_, this->state_.SLNCID == 1); }
+
+void KT0803Component::publish_frequency() { this->publish(this->frequency_number_, this->get_frequency()); }
+
+void KT0803Component::publish(text_sensor::TextSensor *s, const std::string &state) {
+  if (s != nullptr) {
+    if (!s->has_state() || s->state != state) {
+      s->publish_state(state);
+    }
+  }
+}
+
+void KT0803Component::publish(binary_sensor::BinarySensor *s, bool state) {
+ if (s != nullptr) {
+    if (!s->has_state() || s->state != state) {
+      s->publish_state(state);
+    }
+  }
+}
+
+void KT0803Component::publish(sensor::Sensor *s, float state) {
+  if (s != nullptr) {
+    if (!s->has_state() || s->state != state) {
+      s->publish_state(state);
+    }
+  }
+}
+
+void KT0803Component::publish(number::Number *n, float state) {
+  if (n != nullptr) {
+    if (!n->has_state() || n->state != state) {
+      n->publish_state(state);
+    }
+  }
+}
+
+void KT0803Component::publish(switch_::Switch *s, bool state) {
+  if (s != nullptr) {
+    if (s->state != state) {  // ?
+      s->publish_state(state);
+    }
+  }
+}
+
+void KT0803Component::publish(select::Select *s, size_t index) {
+  if (s != nullptr) {
+    if (auto state = s->at(index)) {
+      if (!s->has_state() || s->state != *state) {
+        s->publish_state(*state);
+      }
+    }
+  }
+}
+
+void KT0803Component::publish(text::Text *t, const std::string &state) {
+  if (t != nullptr) {
+    if (!t->has_state() || t->state != state) {
+      t->publish_state(state);
+    }
+  }
+}
+
+}  // namespace kt0803
+}  // namespace esphome
diff --git a/esphome/components/kt0803/kt0803.h b/esphome/components/kt0803/kt0803.h
new file mode 100644
index 0000000000..1e68810dc1
--- /dev/null
+++ b/esphome/components/kt0803/kt0803.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/i2c/i2c.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/text_sensor/text_sensor.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+#include "esphome/components/number/number.h"
+#include "esphome/components/switch/switch.h"
+#include "esphome/components/select/select.h"
+#include "esphome/components/text/text.h"
+#include <string>
+#include "kt0803defs.h"
+
+namespace esphome {
+namespace kt0803 {
+
+#ifndef SUB_TEXT
+#define SUB_TEXT(name) \
+ protected: \
+  text::Text *name##_text_{nullptr}; \
+\
+ public: \
+  void set_##name##_text(text::Text *text) { this->name##_text_ = text; }
+#endif
+
+class KT0803Component : public PollingComponent, public i2c::I2CDevice {
+  ChipId chip_id_; // no way to detect it
+  bool reset_;
+  union {
+    struct KT0803State state_;
+    uint8_t regs_[sizeof(struct KT0803State)];
+  };
+
+  bool check_reg_(uint8_t addr);
+  void write_reg_(uint8_t addr);
+  bool read_reg_(uint8_t addr);
+
+  SUB_BINARY_SENSOR(pw_ok)
+  SUB_BINARY_SENSOR(slncid)
+  SUB_NUMBER(frequency)
+
+  void publish_pw_ok();
+  void publish_slncid();
+  void publish_frequency();
+  
+  void publish(sensor::Sensor *s, float state);
+  void publish(binary_sensor::BinarySensor *s, bool state);
+  void publish(text_sensor::TextSensor *s, const std::string &state);
+  void publish(number::Number *n, float state);
+  void publish(switch_::Switch *s, bool state);
+  void publish(select::Select *s, size_t index);
+  void publish(text::Text *t, const std::string &state);
+
+ public:
+  KT0803Component();
+
+  // float get_setup_priority() const override { return setup_priority::HARDWARE; }
+  void setup() override;
+  void dump_config() override;
+  void update() override;
+  void loop() override;
+
+  void set_chip_id(ChipId value);
+  ChipId get_chip_id();
+  std::string get_chip_string() const;
+
+  void set_frequency(float value);  // MHz
+  float get_frequency();
+};
+
+}  // namespace kt0803
+}  // namespace esphome
diff --git a/esphome/components/kt0803/kt0803defs.h b/esphome/components/kt0803/kt0803defs.h
new file mode 100644
index 0000000000..9d31c491af
--- /dev/null
+++ b/esphome/components/kt0803/kt0803defs.h
@@ -0,0 +1,398 @@
+#pragma once
+
+namespace esphome {
+namespace kt0803 {
+
+static float CHSEL_MIN = 70.0f;
+static float CHSEL_MAX = 108.0f;
+
+static float PGA_MIN = -15;
+static float PGA_MAX = 12;
+
+static float RFGAIN_MIN = 95.5f;
+static float RFGAIN_MAX = 108.0f;
+
+enum class ChipId {
+  KT0803,
+  KT0803K,
+  KT0803M,
+  KT0803L,
+};
+
+enum class PreEmphasis {
+  PHTCNST_50US,
+  PHTCNST_75US,
+  LAST,
+};
+
+enum class PilotToneAmplitudeAdjustment {
+  PLTADJ_LOW,
+  PLTADJ_HIGH,
+  LAST,
+};
+
+enum class BassBoostControl {
+  BASS_DISABLED,
+  BASS_5DB,
+  BASS_11DB,
+  BASS_17DB,
+  LAST,
+};
+
+enum class AlcTimeSelection {
+  ALC_TIME_25US,
+  ALC_TIME_50US,
+  ALC_TIME_75US,
+  ALC_TIME_100US,
+  ALC_TIME_125US,
+  ALC_TIME_150US,
+  ALC_TIME_175US,
+  ALC_TIME_200US,
+  ALC_TIME_50MS,
+  ALC_TIME_100MS,
+  ALC_TIME_150MS,
+  ALC_TIME_200MS,
+  ALC_TIME_250MS,
+  ALC_TIME_300MS,
+  ALC_TIME_350MS,
+  ALC_TIME_40MS,
+  LAST,
+};
+
+enum class SilenceDetectionLowThreshold {
+  SLNCTHL_025MV,
+  SLNCTHL_050MV,
+  SLNCTHL_1MV,
+  SLNCTHL_2MV,
+  SLNCTHL_4MV,
+  SLNCTHL_8MV,
+  SLNCTHL_16MV,
+  SLNCTHL_32MV,
+  LAST,
+};
+
+enum class SilenceDetectionHighThreshold {
+  SLNCTHH_050MV,
+  SLNCTHH_1MV,
+  SLNCTHH_2MV,
+  SLNCTHH_4MV,
+  SLNCTHH_8MV,
+  SLNCTHH_16MV,
+  SLNCTHH_32MV,
+  SLNCTHH_64MV,
+  LAST,
+};
+
+enum class SilenceDetectionHighLevelCounterThreshold {
+  SLNCCNTHIGH_15,
+  SLNCCNTHIGH_31,
+  SLNCCNTHIGH_63,
+  SLNCCNTHIGH_127,
+  SLNCCNTHIGH_255,
+  SLNCCNTHIGH_511,
+  SLNCCNTHIGH_1023,
+  SLNCCNTHIGH_2047,
+  LAST,
+};
+
+enum class SilenceDetectionLowAndHighLevelDurationTime {
+  SLNCTIME_50MS,
+  SLNCTIME_100MS,
+  SLNCTIME_200MS,
+  SLNCTIME_400MS,
+  SLNCTIME_1S,
+  SLNCTIME_2S,
+  SLNCTIME_4S,
+  SLNCTIME_8S,
+  SLNCTIME_16S,
+  SLNCTIME_24S,
+  SLNCTIME_32S,
+  SLNCTIME_40S,
+  SLNCTIME_48S,
+  SLNCTIME_56S,
+  SLNCTIME_60S,
+  SLNCTIME_64S,
+  LAST,
+};
+
+enum class AlcCompressedGainSetting {
+  ALCCMPGAIN_N6DB,
+  ALCCMPGAIN_N9DB,
+  ALCCMPGAIN_N12DB,
+  ALCCMPGAIN_N15DB,
+  ALCCMPGAIN_6DB,
+  ALCCMPGAIN_3DB,
+  ALCCMPGAIN_0DB,
+  ALCCMPGAIN_N3DB,
+  LAST,
+};
+
+enum class SilenceLowCounter {
+  SLNCCNTLOW_1,
+  SLNCCNTLOW_2,
+  SLNCCNTLOW_4,
+  SLNCCNTLOW_8,
+  SLNCCNTLOW_16,
+  SLNCCNTLOW_32,
+  SLNCCNTLOW_64,
+  SLNCCNTLOW_128,
+  LAST,
+};
+
+enum class FrequencyDeviationAdjustment {
+  FDEV_75KHZ,
+  FDEV_112K5HZ,
+  FDEV_150KHZ,
+  FDEV_187K5HZ,
+  LAST,
+};
+
+enum class XtalSelection {
+  XTAL_SEL_32K768HZ,
+  XTAL_SEL_7M6HZ,
+  LAST,
+};
+
+enum class ReferenceClockSelection {
+  REF_CLK_32K768HZ,
+  REF_CLK_6M5HZ,
+  REF_CLK_7M6HZ,
+  REF_CLK_12MHZ,
+  REF_CLK_13MHZ,
+  REF_CLK_15M2HZ,
+  REF_CLK_19M2HZ,
+  REF_CLK_24MHZ,
+  REF_CLK_26MHZ,
+  LAST,
+};
+
+enum class AlcHighThresholdSelection {
+  ALCHIGHTH_50MS,
+  ALCHIGHTH_100MS,
+  ALCHIGHTH_150MS,
+  ALCHIGHTH_200MS,
+  ALCHIGHTH_1S,
+  ALCHIGHTH_5S,
+  ALCHIGHTH_10S,
+  ALCHIGHTH_15S,
+  LAST,
+};
+
+enum class AlcHoldTimeSelection {
+  ALCHOLD_06,
+  ALCHOLD_05,
+  ALCHOLD_04,
+  ALCHOLD_03,
+  ALCHOLD_02,
+  ALCHOLD_01,
+  ALCHOLD_005,
+  ALCHOLD_001,
+  LAST,
+};
+
+enum class AlcLowThreshold {
+  ALCLOWTH_025,
+  ALCLOWTH_020,
+  ALCLOWTH_015,
+  ALCLOWTH_010,
+  ALCLOWTH_005,
+  ALCLOWTH_003,
+  ALCLOWTH_002,
+  ALCLOWTH_001,
+  ALCLOWTH_0005,
+  ALCLOWTH_0001,
+  ALCLOWTH_00005,
+  ALCLOWTH_00001,
+  LAST,
+};
+
+// 0 = KT0803
+// K = KT0803K or KT0803M (not datasheet for M, it should be mostly the same as K)
+// L = KT0803L
+
+struct KT0803State {
+  union {
+    uint8_t REG_00;
+    struct {
+      uint8_t CHSEL1 : 8; // 0 K L
+    };
+  };
+  union {
+    uint8_t REG_01;
+    struct {
+      uint8_t CHSEL2 : 3; // 0 K L
+      uint8_t PGA : 3; // 0 K L
+      uint8_t RFGAIN0 : 2; // 0 K L
+    };
+  };
+  union {
+    uint8_t REG_02;
+    struct {
+      uint8_t PHTCNST : 1; // 0 K L
+      uint8_t _02_1 : 1;
+      uint8_t PLTADJ : 1; // 0 K L
+      uint8_t MUTE : 1; // 0 K L
+      uint8_t _02_2 : 2;
+      uint8_t RFGAIN2 : 1; // 0 K L
+      uint8_t CHSEL0 : 1; // K L (no LSB on 0, step size is only 100KHz)
+    };
+  };
+  uint8_t REG_03;
+  union {
+    uint8_t REG_04;
+    struct {
+      uint8_t BASS : 2; // K L
+      uint8_t FDEV_K : 2; // K
+      uint8_t PGA_LSB : 2; // K L
+      uint8_t MONO : 1; // K L
+      uint8_t ALC_EN : 1; // L
+    };
+  };
+  uint8_t REG_05;
+  uint8_t REG_06;
+  uint8_t REG_07;
+  uint8_t REG_08;
+  uint8_t REG_09;
+  uint8_t REG_0A;
+  union {
+    uint8_t REG_0B;
+    struct {
+      uint8_t _0B_1 : 2;
+      uint8_t AUTO_PADN : 1; // L
+      uint8_t _0B_2 : 2;
+      uint8_t PDPA : 1; // K L
+      uint8_t _0B_3 : 1;
+      uint8_t Standby : 1; // L
+    };
+  };
+  union {
+    uint8_t REG_0C;
+    struct {
+      uint8_t ALC_ATTACK_TIME : 4; // L
+      uint8_t ALC_DECAY_TIME : 4; // L
+    };
+  };
+  union {
+    uint8_t REG_0E;
+    struct {
+      uint8_t _0E_1 : 1;
+      uint8_t PA_BIAS : 1; // K L
+      uint8_t _0E_2 : 6;
+    };
+  };
+  union {
+    uint8_t REG_0F;
+    struct {
+      uint8_t _0F_1 : 2;
+      uint8_t SLNCID : 1; // K L (ro)
+      uint8_t _0F_2 : 1;
+      uint8_t PW_OK : 1; // K L (ro)
+      uint8_t _0F_3 : 3;
+    };
+  };
+  union {
+    uint8_t REG_10;
+    struct {
+      uint8_t PGAMOD : 1; // K L
+      uint8_t _10_1 : 2;
+      uint8_t LMTLVL : 2; // K
+      uint8_t _10_2 : 4;
+    };
+  };
+  uint8_t REG_11;
+  union {
+    uint8_t REG_12;
+    struct {
+      uint8_t SW_MOD : 1; // K L
+      uint8_t SLNCTHH : 3; // K L
+      uint8_t SLNCTHL : 3; // K L
+      uint8_t SLNCDIS : 1; // K L
+    };
+  };
+  union {
+    uint8_t REG_13;
+    struct {
+      uint8_t _13_1 : 2;
+      uint8_t PA_CTRL : 1; // 0 K L
+      uint8_t _13_2 : 4;
+      uint8_t RFGAIN1 : 1; // 0 K L
+    };
+  };
+  union {
+    uint8_t REG_14;
+    struct {
+      uint8_t SLNCTIME_MSB : 1; // L
+      uint8_t _14_1 : 1;
+      uint8_t SLNCCNTHIGH : 3; // K L
+      uint8_t SLNCTIME : 3; // K L
+    };
+  };
+  union {
+    uint8_t REG_15;
+    struct {
+      uint8_t _15_1 : 5;
+      uint8_t ALCCMPGAIN : 3; // L
+    };
+  };
+  union {
+    uint8_t REG_16;
+    struct {
+      uint8_t SLNCCNTLOW : 3; // K L
+      uint8_t _16_1 : 5;
+    };
+  };
+  union {
+    uint8_t REG_17;
+    struct {
+      uint8_t _17_1 : 3;
+      uint8_t XTAL_SEL : 1; // L
+      uint8_t _17_2 : 1;
+      uint8_t AU_ENHANCE : 1; // L
+      uint8_t FDEV_L : 1; // L
+      uint8_t _17_3 : 1;
+    };
+  };
+  uint8_t REG_18;
+  uint8_t REG_19;
+  uint8_t REG_1A;
+  uint8_t REG_1B;
+  uint8_t REG_1C;
+  uint8_t REG_1D;
+  union {
+    uint8_t REG_1E;
+    struct {
+      uint8_t _1E_1 : 1;
+      uint8_t REF_CLK : 3; // L
+      uint8_t _1E_2 : 1;
+      uint8_t XTALD : 1; // L
+      uint8_t DCLK : 1; // L
+      uint8_t _1E_3 : 1;
+    };
+  };
+  uint8_t REG_1F;
+  uint8_t REG_20;
+  uint8_t REG_21;
+  uint8_t REG_22;
+  uint8_t REG_23;
+  uint8_t REG_24;
+  uint8_t REG_25;
+  union {
+    uint8_t REG_26;
+    struct {
+      uint8_t _26_1 : 1;
+      uint8_t ALCHIGHTH : 3; // L
+      uint8_t _26_2 : 1;
+      uint8_t ALCHOLD : 3; // L
+    };
+  };
+  union {
+    uint8_t REG_27;
+    struct {
+      uint8_t ALCLOWTH : 4; // L
+      uint8_t _27_1 : 4;
+    };
+  };
+};
+
+}  // namespace kt0803
+}  // namespace esphome
diff --git a/esphome/components/kt0803/number/__init__.py b/esphome/components/kt0803/number/__init__.py
new file mode 100644
index 0000000000..68b84b0695
--- /dev/null
+++ b/esphome/components/kt0803/number/__init__.py
@@ -0,0 +1,38 @@
+import esphome.codegen as cg
+from esphome.components import number
+import esphome.config_validation as cv
+from esphome.const import (
+    CONF_FREQUENCY,
+    DEVICE_CLASS_FREQUENCY,
+    ENTITY_CATEGORY_CONFIG,
+)
+from .. import (
+    CONF_KT0803_ID,
+    KT0803Component,
+    kt0803_ns,
+    UNIT_MEGA_HERTZ,
+)
+
+FrequencyNumber = kt0803_ns.class_("FrequencyNumber", number.Number)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(CONF_KT0803_ID): cv.use_id(KT0803Component),
+        cv.Optional(CONF_FREQUENCY): number.number_schema(
+            FrequencyNumber,
+            unit_of_measurement=UNIT_MEGA_HERTZ,
+            device_class=DEVICE_CLASS_FREQUENCY,
+            entity_category=ENTITY_CATEGORY_CONFIG,
+        ),
+    }
+)
+
+
+async def to_code(config):
+    kt0803_component = await cg.get_variable(config[CONF_KT0803_ID])
+    if frequency_config := config.get(CONF_FREQUENCY):
+        n = await number.new_number(
+            frequency_config, min_value=70, max_value=108, step=0.05
+        )
+        await cg.register_parented(n, config[CONF_KT0803_ID])
+        cg.add(kt0803_component.set_frequency_number(n))
diff --git a/esphome/components/kt0803/number/frequency_number.cpp b/esphome/components/kt0803/number/frequency_number.cpp
new file mode 100644
index 0000000000..30cb39e8b5
--- /dev/null
+++ b/esphome/components/kt0803/number/frequency_number.cpp
@@ -0,0 +1,12 @@
+#include "frequency_number.h"
+
+namespace esphome {
+namespace kt0803 {
+
+void FrequencyNumber::control(float value) {
+  this->publish_state(value);
+  this->parent_->set_frequency(value);
+}
+
+}  // namespace kt0803
+}  // namespace esphome
diff --git a/esphome/components/kt0803/number/frequency_number.h b/esphome/components/kt0803/number/frequency_number.h
new file mode 100644
index 0000000000..ba7c69b49b
--- /dev/null
+++ b/esphome/components/kt0803/number/frequency_number.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "esphome/components/number/number.h"
+#include "../kt0803.h"
+
+namespace esphome {
+namespace kt0803 {
+
+class FrequencyNumber : public number::Number, public Parented<KT0803Component> {
+ public:
+  FrequencyNumber() = default;
+
+ protected:
+  void control(float value) override;
+};
+
+}  // namespace kt0803
+}  // namespace esphome