Using MQTT with the Raspberry Pi Pico W and HomeAssistant for an Optimized Solar Energy Electrical Vehicle Charger

I’m in the final stage of finishing a electrical vehicle (EV) charger controller, which optimizes battery loading using the available PV system: use as much as possible the solar energy and not the grid.

Raspberry Pi Pico W as a EV Charger Controller

While the controller talks with an Modbus (RS-485) interface to the vehicle charger itself (see Controlling an EV Charger with Modbus RTU), it uses MQTT over WiFi to get information about the available solar energy from HomeAssistant and the Powerwall.


The hardware is based on a Raspberry Pi Pico W board, 4 WS2812B addressable WRGB LEDs for status, a 4-way+center navigation switch with an OLED as display for status and as user interface. The application runs FreeRTOS, and the GUI is implemented with lvgl.

EVCC Components

The Raspberry Pi Pico uses the SDK 1.4.

💡 At writing of this article, the SDK 1.5 just has been made available. I have not tried that new SDK yet.

The PCB has been designed in KiCAD:


To protect the electronics, I have created an enclosure using 3 mm PMMA with the laser cutter:

The unit controls a Heidelberg Energy Control EV charger over Modbus (see Controlling an EV Charger with Modbus RTU).

MQTT is used to talk to the home automation system implemented with HomeAssistant. With the MQTT protocol, the controller receives information about the solar energy produced and how much is used by the building. That way the system can adjust the charging levels to maximize usage of solar energy for the charging process: instead feeding energy to the grid, it is used first to charge the vehicle battery.

All files can be found on Github (see links at the end of this article).

MQTT Broker

First, I need a server to provide me the data (topics). I’m using here HomeAssistant running on a Rasperry Pi 4, but you can use any kind of MQTT broker. In my case I’m using the ‘Mosquitto broker’ in HomeAssistant to publish values from the Tesla Powerwall gateway:

Mosquitto Broker in HomeAssistant

For the MQTT connection, we need the login information for the MQTT broker. This is found behind the ‘Configure’:

Then use ‘Re-Configure MQTT’:

Then you can set or get the username and password:

The Tesla Powerwall status and data is available within the HomeAssistant integration:

Values of PowerWall in HomeAssistant

To publish the topics with MQTT, you have to edit the /config/configuration.yaml. Below is my configuration which publishes my topics:

  base_topic: homeassistant
  publish_attributes: true
  publish_timestamps: true
      - sensor.powerwall_solar_now
      - sensor.powerwall_load_now
      - sensor.powerwall_site_now
      - sensor.powerwall_battery_now
      - sensor.powerwall_charge
      - binary_sensor.powerwall_charging
      - binary_sensor.powerwall_connected_to_tesla

I recommend verifying the topics, for example with MQTT Explorer:

MQTT Explorer

MQTT Client Files

The MQTT client is using lwIP and the Raspberry Pi Pico (W) SDK. Below are the most important files (see link to GitHub at the end of the article):

  • mqtt.c: lwip MQTT functions, based on lwIP example
  • dns_resolver.c: custom domain name server resolver with lwIP. Translates host names into IP addresses.
  • mqtt_client.c: MQTT client implementation

User names and passwords are not stored in the source files (only dummy values): the real values are stored using MinINI on the device itself.


The lwipopts.h configures the lwIP network stack. If using it with MQTT, there are two critical settings to add, otherwise it won’t work:

/* You need to increase MEMP_NUM_SYS_TIMEOUT by one if you use MQTT!
 * see
#define MQTT_REQ_MAX_IN_FLIGHT  (5) /* maximum of subscribe requests */

The last item is about the number of subscribe requests, and I’m using 5 in my application.

Connect to Broker

To connect to the broker, I need a handle:

static mqtt_t mqtt; /* information used for MQTT connection */

The handle gets initialized as below:

mqtt_client = mqtt_client_new(); /* create client handle */

Connection information is stored inside a struct:

typedef struct mqtt_t {
  mqtt_client_t *mqtt_client;       /* lwIP MQTT client handle */
  DnsResolver_info_t addr;          /* broker lwip address, resolved by DNS if hostname is used */
  unsigned char broker[32];         /* broker IP or hostname string. For hostname, DNS will be used */
  unsigned char client_id[32];      /* client ID used for connection */
  unsigned char client_user[32];    /* client user name used for connection */
  unsigned char client_pass[96];    /* client user password */
  topic_ID_e in_pub_ID;             /* incoming published ID, set in the incoming_publish_cb and used in the incoming_data_cb */
} mqtt_t;

static mqtt_t mqtt; /* information used for MQTT connection */

For the lwIP MQTT connection, I have to resolve first the broker hostname (e.g. “myServer”) to an IP address (e.g. For this, a little DNS resolver is used:

  /* resolve hostname to IP address: */
  if (DnsResolver_ResolveName(, &mqtt.addr, 1000)!=0) { /* use DNS to resolve name to IP address */
    McuLog_error("failed to resolve broker name %s",;

Then I register the connection callbacks:

  /* setup callbacks for incoming data: */
      mqtt_client, /* client handle */
      mqtt_incoming_publish_cb, /* callback for incoming publish messages */
      mqtt_incoming_data_cb, /* callback for incoming data */
      LWIP_CONST_CAST(void*, &mqtt_client_info) /* argument for callbacks */

For the connection I need a struct with the client information:

static const struct mqtt_connect_client_info_t mqtt_client_info = {
  mqtt.client_id, /* client ID */
  mqtt.client_user, /* client user name */
  mqtt.client_pass, /* client password */
  100,  /* keep alive timeout in seconds */
  NULL, /* will_topic */
  NULL, /* will_msg */
  0,    /* will_qos */
  0     /* will_retain */
  , NULL

Using this, I can make the connection to the broker:

  /* connect to broker */
  cyw43_arch_lwip_begin(); /* start section for to lwIP access */
      mqtt_client, /* client handle */
      &mqtt.addr.resolved_addr, /* broker IP address */
      MQTT_PORT, /* port to be used */
      mqtt_connection_cb, LWIP_CONST_CAST(void*, &mqtt_client_info), /* connection callback with argument */
      &mqtt_client_info /* client information */
  cyw43_arch_lwip_end(); /* end section accessing lwIP */

Connection Callback

If connection is successful, the connection callback I have provided during mqtt_client_connect() gets called. In that callback I can register to the topics:

static void mqtt_connection_cb(mqtt_client_t *client, void *arg, mqtt_connection_status_t status) {
  const struct mqtt_connect_client_info_t *client_info = (const struct mqtt_connect_client_info_t*)arg;
  err_t err;

  McuLog_trace("MQTT client \"%s\" connection cb: status %d", client_info->client_id, (int)status);
  if (status!=MQTT_CONNECT_ACCEPTED) {
    McuLog_error("MQTT client \"%s\" connection cb: FAILED status %d", client_info->client_id, (int)status);
  /* subscribe to topics */
  if (status == MQTT_CONNECT_ACCEPTED) {
    McuLog_trace("MQTT connect accepted");
    err = mqtt_sub_unsub(client,
            TOPIC_NAME_SOLAR_POWER, /* solar P in kW */
            1, /* quos: 0: fire&forget, 1: at least once */
            mqtt_request_cb, /* callback */
            LWIP_CONST_CAST(void*, client_info),
            1 /* subscribe */
    if (err!=ERR_OK) {
      McuLog_error("failed subscribing, err %d", err);
  } else if (status==MQTT_CONNECT_DISCONNECTED) {
    McuLog_trace("MQTT connect disconnect");

The subscribe topics look like this:

/* HomeAssistant Tesla Powerwall topics */
#define TOPIC_NAME_SOLAR_POWER          "homeassistant/sensor/powerwall_solar_now/state"
#define TOPIC_NAME_SITE_POWER           "homeassistant/sensor/powerwall_load_now/state"
#define TOPIC_NAME_GRID_POWER           "homeassistant/sensor/powerwall_site_now/state"
#define TOPIC_NAME_BATTERY_POWER        "homeassistant/sensor/powerwall_battery_now/state"
#define TOPIC_NAME_BATTERY_PERCENTAGE   "homeassistant/sensor/powerwall_charge/state"

Publishing and Data Callbacks

The data for the subscribed topics comes with two callbacks: first with the mqtt_incoming_publish_cb() and then the data with mqtt_incoming_data_cb().

To distinguish between the different topics, I’m using a list of IDs :

typedef enum topic_ID_e {
  Topic_ID_Solar_Power,       /* power from PV panels */
  Topic_ID_Site_Power,        /* power to the house/site */
  Topic_ID_Grid_Power,        /* power from/to grid */
  Topic_ID_Battery_Power,     /* power from/to battery */
  Topic_ID_Battery_Percentage,/* battery level percentage */
} topic_ID_e;

The IDs get assigned based on the topic string:

static void mqtt_incoming_publish_cb(void *arg, const char *topic, u32_t tot_len) {
  const struct mqtt_connect_client_info_t *client_info = (const struct mqtt_connect_client_info_t*)arg;

  if (McuUtility_strcmp(topic, TOPIC_NAME_SOLAR_POWER)==0) {
    mqtt.in_pub_ID = Topic_ID_Solar_Power;
  } else if (McuUtility_strcmp(topic, TOPIC_NAME_SITE_POWER)==0) {
    mqtt.in_pub_ID = Topic_ID_Site_Power;
  } else if (McuUtility_strcmp(topic, TOPIC_NAME_GRID_POWER)==0) {
    mqtt.in_pub_ID = Topic_ID_Grid_Power;
  } else if (McuUtility_strcmp(topic, TOPIC_NAME_BATTERY_POWER)==0) {
    mqtt.in_pub_ID = Topic_ID_Battery_Power;
  } else if (McuUtility_strcmp(topic, TOPIC_NAME_BATTERY_PERCENTAGE)==0) {
    mqtt.in_pub_ID = Topic_ID_Battery_Percentage;
  } else { /* unknown */
    McuLog_trace("MQTT client \"%s\" publish cb: topic %s, len %d", client_info->client_id, topic, (int)tot_len);
    mqtt.in_pub_ID = Topic_ID_None;

The ID is then used in the data callback:

static void mqtt_incoming_data_cb(void *arg, const u8_t *data, u16_t len, u8_t flags) {
  const struct mqtt_connect_client_info_t *client_info = (const struct mqtt_connect_client_info_t*)arg;
  unsigned char buf[32];
  int32_t watt;

  McuLog_trace("MQTT client \"%s\" data cb: len %d, flags %d", client_info->client_id, (int)len, (int)flags);
  if(flags & MQTT_DATA_FLAG_LAST) {
    /* Last fragment of payload received (or whole part if payload fits receive buffer. See MQTT_VAR_HEADER_BUFFER_LEN)  */
    if(mqtt.in_pub_ID == Topic_ID_Solar_Power) {
      GetDataString(buf, sizeof(buf), data, len);
      McuLog_trace("solarP: %s", buf);
      watt = scanWattValue(buf);
      if (watt>=0) { /* can only be positive */
    } else if(mqtt.in_pub_ID == Topic_ID_Site_Power) {
      GetDataString(buf, sizeof(buf), data, len);
      McuLog_trace("siteP: %s", buf);
      watt = scanWattValue(buf);
      if (watt>=0) { /* can only be positive */
    } else if(mqtt.in_pub_ID == Topic_ID_Grid_Power) {
      GetDataString(buf, sizeof(buf), data, len);
      McuLog_trace("gridP: %s", buf);
    } else if(mqtt.in_pub_ID == Topic_ID_Battery_Power) {
      GetDataString(buf, sizeof(buf), data, len);
      McuLog_trace("battP: %s", buf);
    } else if(mqtt.in_pub_ID == Topic_ID_Battery_Percentage) {
      GetDataString(buf, sizeof(buf), data, len);
      McuLog_trace("bat : %s%%", buf);
    } else {
      McuLog_trace("mqtt_incoming_data_cb: Ignoring payload...");
  } else {
    McuLog_trace("mqtt_incoming_data_cb: fragmented payload ...");
    /* Handle fragmented payload, store in buffer, write to file or whatever */

With this, an example session then looks like this:

01.01.2020 00:00:00,00 INFO  PicoWiFi.c:99: started WiFi task
01.01.2020 00:00:00,00 INFO  PicoWiFi.c:106: enabling STA mode
01.01.2020 00:00:00,00 TRACE Shell.c:194: started shell task
01.01.2020 00:00:00,22 INFO  McuHeidelberg.c:631: connected with charger
01.01.2020 00:00:00,88 INFO  PicoWiFi.c:117: setting hostname: pico
01.01.2020 00:00:01,88 INFO  PicoWiFi.c:122: connecting to SSID '2.4GHz'...
01.01.2020 00:00:05,60 INFO  PicoWiFi.c:130: success!
01.01.2020 00:00:05,61 INFO  dns_resolver.c:20: 'homeassistant' resolved to
01.01.2020 00:00:05,62 INFO  dns_resolver.c:20: '' resolved to
01.01.2020 00:00:05,67 INFO  ntp_client.c:42: got ntp response: 11/02/2023 18:17:59
11.02.2023 18:17:59,52 TRACE mqtt_client.c:183: MQTT connect accepted
11.02.2023 18:17:59,57 TRACE mqtt_client.c:114: solarP: 0.0
11.02.2023 18:17:59,58 TRACE mqtt_client.c:121: siteP: 0.2
11.02.2023 18:17:59,58 TRACE mqtt_client.c:128: gridP: -0.006
11.02.2023 18:17:59,58 TRACE mqtt_client.c:131: battP: 0.2
11.02.2023 18:17:59,58 TRACE mqtt_client.c:134: bat : 96%
11.02.2023 18:18:17,02 TRACE mqtt_client.c:128: gridP: -0.036
11.02.2023 18:18:17,02 TRACE mqtt_client.c:131: battP: 0.21
11.02.2023 18:18:17,02 TRACE mqtt_client.c:121: siteP: 0.19


MQTT is not part of the Pico W SDK 1.4, but with a few tweaks I was able to get it working. Now I have a charger controller talking with MQTT with the HomeAssistant, and I can exchange any kind of data I like.

I hope this helps you to bring MQTT to your Pico W projects too. In any case: check the latest 1.5 SDK as it says:

Added pico_lwip_mqtt library to expose the MQTT app functionality in lwIP.

If you already jumped on 1.5 SDK and using it for MQTT, let me know.

Sources, project files including the enclosure files are available on Github.

Happy MQTTing 🙂



9 thoughts on “Using MQTT with the Raspberry Pi Pico W and HomeAssistant for an Optimized Solar Energy Electrical Vehicle Charger

      • I just had to edit my lwipopts.h to run the mqtt client.
        Then the client send the ambient Sensor Data to my Influx Database/ Grafana Dashboard, running on a Raspberry Pi.
        So far the code was running stable. I still have do run a long term test.


        • My Project was already running under SDK 1.4.0.
          When updating to 1.5.0
          I removed some custom links for mqtt library, which broke my Project.
          Now my Project is compiling and running with SDK 1.5.0


    • Hey, I can’t seem to keep the connection working once it’s established, I receive MQTT_Connected status but just after it the connection callback gets called once again indicating TCP_Disconnected for the client. I am using SDK version 1.5.0


      • Hi Mousa,
        I have switched to SDK 1.5, and everything works as in 1.4.
        I suggest that you compare your lwipopts.h with the one I have on GitHub, probably you missed a setting there, especially the MEMP_NUM_SYS_TIMEOUT setting.


        • Hello Erich,
          I have managed to make it work. The reason was actually the options LWIP_ALTCP, LWIP_ALTCP_TLS,LWIP_ALTCP_TLS_MBEDTLS being set to 1 when using a broker that is not using any certificates for communication. However, I am now using a broker on AWS IOT, but with even using the certificates I can’t connect


        • So you are saying it works now for you in one case, but not with AWS IOT? I have to admit that I stopped using AWS for this, so probably won’t be of any help for things around AWS.


What do you think?

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.