Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ba2027d
Added 128x64 screen support
hackthis02 Apr 16, 2024
e494c07
Added 128x64 screen support
hackthis02 Apr 16, 2024
57fcc36
Added stats screen info
hackthis02 Apr 18, 2024
07b071b
Fixed config typo
hackthis02 Apr 18, 2024
c83f802
fixed argument order
hackthis02 Apr 18, 2024
7bfd5ea
fixed inconsistent use of tabs and spaces in indentation
hackthis02 Apr 18, 2024
4ff3996
Testing size select
hackthis02 Apr 18, 2024
9f76b99
fixed if check
hackthis02 Apr 18, 2024
07d0049
fixed display init
hackthis02 Apr 18, 2024
9b95243
fixed display init
hackthis02 Apr 18, 2024
3048276
fixed display peram order
hackthis02 Apr 18, 2024
1b6b146
updated readme
hackthis02 Apr 18, 2024
de63b67
Merge with forked
hackthis02 May 20, 2025
99b88bc
Formatting and display args
hackthis02 May 20, 2025
61862c2
Fixing display args
hackthis02 May 20, 2025
49ff7c1
Changing display args
hackthis02 May 20, 2025
45fc6d3
Added logging
hackthis02 May 20, 2025
4a032e5
Trying to fix regex warning.
hackthis02 May 20, 2025
2f30eaa
Removed unused logging.
hackthis02 May 20, 2025
0de88f7
Changed from text to icons
hackthis02 Jun 20, 2025
e7c696d
Added stats icon option
hackthis02 Jun 20, 2025
61d56b7
Merge pull request #1 from hackthis02/icon
hackthis02 Jun 20, 2025
d21d282
fixed keys typo
hackthis02 Jun 20, 2025
1da288c
removed text
hackthis02 Jun 20, 2025
7486f95
fixed parent attribute
hackthis02 Jun 20, 2025
6cf3d5e
fixed icon coordinates
hackthis02 Jun 20, 2025
a6cbb7d
added icon font
hackthis02 Jun 20, 2025
844a587
fixing decoding error
hackthis02 Jun 20, 2025
023f299
working on icon text
hackthis02 Jun 20, 2025
bc70e4e
fixed formating
hackthis02 Jun 20, 2025
33c3cc2
text formating
hackthis02 Jun 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
__pycache__
/.vs
Empty file modified LICENSE
100644 → 100755
Empty file.
26 changes: 18 additions & 8 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
I2C OLED Controller for Raspberry Pi
====================================

Python library to enable 128x32 pixel OLED for Raspberry Pi (both 32 and 64-bit) that utilize the SSD1306 chipset. This works as a standalone service and can run on a standard Raspberry Pi running Raspian.
Python library to enable 128x32 or 64 pixel OLED for Raspberry Pi (both 32 and 64-bit) that utilize the SSD1306 chipset. This works as a standalone service and can run on a standard Raspberry Pi running Raspian.

**This addon leverages the original [Adafruit Python SSD1306](https://github.com/adafruit/Adafruit_Python_SSD1306) and [GPIO](https://github.com/adafruit/Adafruit_Python_GPIO) libraries, which have been deprecated. However, I have taken the nessassary parts out of this and bundled them into this I2C module avoiding the need for GPIO and relying on the Raspberry Pi's I2C setup.**

Expand All @@ -14,11 +14,13 @@ Python library to enable 128x32 pixel OLED for Raspberry Pi (both 32 and 64-bit)

## Some Teaser Screenshots

| Welcome | HA Splash | CPU | Memory | Storage | Network | Exit Screen |
|-----------|-----------|-----------|-----------|---------------|---------------|---------------|
| ![Welcome][welcome-url] | ![Splash][splash-url] | ![CPU Stats][cpu-stats-url] | ![RAM Stats][ram-stats-url] | ![Storage Stats][storage-stats-url] | ![Network Stats][network-stats-url] | ![Exit][exit-url] |
| Welcome | HA Splash | CPU | Memory | Storage | Network | Exit Screen |Stats Screen* |
|-----------|-----------|-----------|-----------|---------------|---------------|---------------|---------------|
| ![Welcome][welcome-url] | ![Splash][splash-url] | ![CPU Stats][cpu-stats-url] | ![RAM Stats][ram-stats-url] | ![Storage Stats][storage-stats-url] | ![Network Stats][network-stats-url] | ![Exit][exit-url] | ![Stats][stats-url] |
| | | ![CPU Stats][cpu-stats-url-icon] | ![RAM Stats][ram-stats-url-icon] | ![Storage Stats][storage-stats-url-icon] | ![Network Stats][network-stats-url-icon] | |
| | | ![CPU Stats][cpu-stats-url-compact] | ![RAM Stats][ram-stats-url-compact] | ![Storage Stats][storage-stats-url-compact] | ![Network Stats][network-stats-url-compact] | |
| | | ![CPU Stats][cpu-stats-url-compact] | ![RAM Stats][ram-stats-url-compact] | ![Storage Stats][storage-stats-url-compact] | ![Network Stats][network-stats-url-compact] | ![Stats][stats-url] |

*Stats screen designed for 128x64 displays only

## Custom Screen & Static Text Variables

Expand All @@ -41,8 +43,9 @@ The following variables are supported
| {datetime} | Displays the current datetime based on the defined format specified in the ```DateTime_Format``` config option. |
| {hostname} | Displays the current hostname of the host device |
| {ip} | Displays the host device IP |
| {hassio.info.property} | Fetches a specified property from Home Assistants supervisor API (e.g. http://supervisor/os/info). You can state the namespace and property which will populate with the responding value. This must be fixed with hassio first, followed by the namespace (e.g. os, network etc), then the property e.g. hassio.os.latest_version will call http://supervisor/os/info and display the ```latest_version``` value. |
| {hassio.info.property}* | Fetches a specified property from Home Assistants supervisor API (e.g. http://supervisor/os/info). You can state the namespace and property which will populate with the responding value. This must be fixed with hassio first, followed by the namespace (e.g. os, network etc), then the property e.g. hassio.os.latest_version will call http://supervisor/os/info and display the ```latest_version``` value. |

*Some properties may not be available without setting up the API access in Home Assistant and inputting the supervisor token into configuration. This is not required for the basic screens.
<br>
<br>

Expand All @@ -56,9 +59,13 @@ The Home Assistant add-on that uses this can be accessed from [HomeAssistant_Add

Hardware Setup
===============
You can use 0.91 Inch 128x32 I2C module, as long as it is registered on /dev/i2c-1 which is the Rasperry Pi default.
You can use 0.91 Inch 128x32 or 64 I2C module, as long as it is registered on /dev/i2c-1 which is the Rasperry Pi default.

I purchased this [MakerHawk I2C OLED Display Module I2C Screen Module 0.91" 128x32 I2C](https://www.amazon.co.uk/gp/product/B07BDFXFRK/ref=ppx_yo_dt_b_asin_title_o07_s00?ie=UTF8&psc=1)
Testing Hardware
<br>
[MakerHawk I2C OLED Display Module I2C Screen Module 0.91" 128X32 I2C](https://amzn.eu/d/cCNIybv)
<br>
[128x64 Pixels IIC 3.3V 5V White Character Screen Module](https://a.co/d/cTte8OO)

| Pin | Details |
|:---:|--------------------|
Expand Down Expand Up @@ -165,6 +172,7 @@ python3 display.py --config /path/to/options.json

| Name | Type | Requirement | Description | Default |
| ---------------------| ------- | ------------ | -------------------------------------------------------| ------------------- |
| Screen_Size | string | **Required** | The size of the screen you want to display. Currently only 128x32 and 128x64 are supported| `32` |
| i2c_bus | int | **Required** | I2C bus number. /dev/i2c-[bus number] | `1` |
| Temperature_Unit | string | **Required** | Display the CPU temperature in C or F | `C` |
| Rotate | int | **Optional** | Rotates the screen by the number of degrees provided counter clockwise around its centre (e.g. 180 displays the screen upside down). | 0 |
Expand Down Expand Up @@ -264,3 +272,5 @@ I2C](https://www.amazon.co.uk/gp/product/B07BDFXFRK/)
[ram-stats-url-icon]: /img/examples/icon/memory.png?raw=true
[storage-stats-url-icon]: /img/examples/icon/storage.png?raw=true
[network-stats-url-icon]: /img/examples/icon/network.png?raw=true

[stats-url]: /img/examples/stats.png?raw=true
Empty file modified __init__.py
100644 → 100755
Empty file.
14 changes: 10 additions & 4 deletions bin/Config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class Config:
'storage',
'memory',
'cpu',
'static'
'static',
'stats'
]
HASSIO_DEPENDENT_SCREENS = [
'Splash'
Expand All @@ -38,7 +39,10 @@ class Config:
'rotate': 'rotate',
'show_icons': 'show_icons',
'show_hint': 'show_hint',
'compact': 'compact'
'compact': 'compact',
'supervizor_token': 'supervizor_token',
'screen_size': 'screen_size',
'icon_stats': 'icon_stats'
}

logger = logging.getLogger('Config')
Expand Down Expand Up @@ -118,17 +122,19 @@ def _init_display(self):
show_icons = self.get_option_value('show_icons')
show_hint = self.get_option_value('show_hint')
compact = self.get_option_value('compact')
size = self.get_option_value('screen_size')
icon_stats = self.get_option_value('icon_stats')

self.display = Display(busnum=busnum, screenshot=screenshot,
rotate=rotate, show_icons=show_icons,
rotate=rotate, size = size, icon_stats=icon_stats, show_icons=show_icons,
show_hint=show_hint, compact=compact)

except Exception as e:
raise Exception("Could not create display. Check your i2c bus with 'ls /dev/i2c-*'.")

def _init_utils(self):
if self.is_hassio_supported:
self.utils = HassioUtils
self.utils = HassioUtils()
else:
self.utils = Utils

Expand Down
41 changes: 41 additions & 0 deletions bin/SSD1306.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,47 @@ def image(self, image):
def clear(self):
"""Clear contents of image buffer."""
self._buffer = [0]*(self.width*self._pages)

class SSD1306_128_64(SSD1306Base):
def __init__(self, busnum=1, i2c_address=SSD1306_I2C_ADDRESS):
# Call base class constructor.
super(SSD1306_128_64, self).__init__(128, 64, i2c_address, busnum)

def _initialize(self):
# 128x64 pixel specific initialization.
self.command(SSD1306_DISPLAYOFF) # 0xAE
self.command(SSD1306_SETDISPLAYCLOCKDIV) # 0xD5
self.command(0x80) # the suggested ratio 0x80
self.command(SSD1306_SETMULTIPLEX) # 0xA8
self.command(0x3F)
self.command(SSD1306_SETDISPLAYOFFSET) # 0xD3
self.command(0x0) # no offset
self.command(SSD1306_SETSTARTLINE | 0x0) # line #0
self.command(SSD1306_CHARGEPUMP) # 0x8D
if self._vccstate == SSD1306_EXTERNALVCC:
self.command(0x10)
else:
self.command(0x14)
self.command(SSD1306_MEMORYMODE) # 0x20
self.command(0x00) # 0x0 act like ks0108
self.command(SSD1306_SEGREMAP | 0x1)
self.command(SSD1306_COMSCANDEC)
self.command(SSD1306_SETCOMPINS) # 0xDA
self.command(0x12)
self.command(SSD1306_SETCONTRAST) # 0x81
if self._vccstate == SSD1306_EXTERNALVCC:
self.command(0x9F)
else:
self.command(0xCF)
self.command(SSD1306_SETPRECHARGE) # 0xd9
if self._vccstate == SSD1306_EXTERNALVCC:
self.command(0x22)
else:
self.command(0xF1)
self.command(SSD1306_SETVCOMDETECT) # 0xDB
self.command(0x40)
self.command(SSD1306_DISPLAYALLON_RESUME) # 0xA4
self.command(SSD1306_NORMALDISPLAY) # 0xA6

class SSD1306_128_32(SSD1306Base):
def __init__(self, busnum=1, i2c_address=SSD1306_I2C_ADDRESS):
Expand Down
80 changes: 70 additions & 10 deletions bin/Screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,26 @@
from PIL import Image, ImageDraw, ImageFont, ImageOps

from bin.Scroller import Scroller
from bin.SSD1306 import SSD1306_128_32 as SSD1306
from bin.Utils import Utils
from bin.SSD1306 import SSD1306_128_64, SSD1306_128_32
from bin.Utils import Utils, HassioUtils




class Display:
DEFAULT_BUSNUM = 1
SCREENSHOT_PATH = "./img/examples/"

def __init__(self, busnum = None, screenshot = False, rotate = False, show_icons = True,
compact = False, show_hint = False):
def __init__(self, busnum = None, screenshot = False, rotate = False, config = None, show_icons = True,
compact = False, show_hint = False, size = '32', icon_stats = 'text'):
self.logger = logging.getLogger('Display')

if not isinstance(busnum, int):
busnum = Display.DEFAULT_BUSNUM

self.display = SSD1306(busnum)
if size == '64':
self.display = SSD1306_128_64(busnum)
else:
self.display = SSD1306_128_32(busnum)
self.clear()
self.width = self.display.width
self.height = self.display.height
Expand All @@ -29,6 +33,7 @@ def __init__(self, busnum = None, screenshot = False, rotate = False, show_icons
self.compact = compact
self.show_icons = show_icons
self.show_hint = show_hint
self.icon_stats = icon_stats
self.hint_right = True

if self.show_icons and self.show_hint:
Expand Down Expand Up @@ -67,8 +72,9 @@ def capture_screenshot(self, name):
self.image.save(path)

class BaseScreen:
font_path = Utils.current_dir + "/fonts/DejaVuSans.ttf"
font_path = Utils.current_dir + "/fonts/PixelOperator.ttf"
font_bold_path = Utils.current_dir + "/fonts/DejaVuSans-Bold.ttf"
font_icon = Utils.current_dir + "/fonts/lineawesome-webfont.ttf"
fonts = {}

def __init__(self, duration, display = Display(), utils = Utils(), config = None):
Expand Down Expand Up @@ -180,21 +186,25 @@ def display_hint(self):
if (len(self.hint) > 2):
draw.text((x_pos, 18), self.hint[2], font=font, fill=text_fill)

def font(self, size = None, is_bold = False):
def font(self, size = None, is_bold = False, is_icon = False):
# default to the current screen's font size if none provided
if not size:
size = self.font_size

suffix = None
if is_bold:
suffix = '_bold'
elif is_icon:
suffix = '_icon'

key = 'font_{}{}'.format(str(size), suffix)

if key not in BaseScreen.fonts:
font = BaseScreen.font_path
if is_bold:
font = BaseScreen.font_bold_path
elif is_icon:
font = BaseScreen.font_icon

font = ImageFont.truetype(font, int(size))
BaseScreen.fonts[key] = font
Expand Down Expand Up @@ -364,14 +374,14 @@ def render(self):
Home Assistant screen.
If you're not using Home Assistant OS, disable this screen in the config
'''
os_info = self.utils.hassos_get_info('os/info')
os_info = self.utils.hassos_get_info(self, 'os/info')
os_version = os_info['data']['version']
os_upgrade = os_info['data']['update_available']

if (os_upgrade == True):
os_version = os_version + "*"

core_info = self.utils.hassos_get_info('core/info')
core_info = self.utils.hassos_get_info(self, 'core/info')
core_version = core_info['data']['version']
core_upgrade = os_info['data']['update_available']
if (core_upgrade == True):
Expand Down Expand Up @@ -502,3 +512,53 @@ def render(self):

self.render_with_defaults()



class StatsScreen(BaseScreen):
def set_temp_unit(self, unit):
unit = str(unit).upper()
if unit in ['C', 'F']:
self.temp_unit = unit

def get_temp(self):
temp = float(Utils.shell_cmd("cat /sys/class/thermal/thermal_zone0/temp")) / 1000.00

if (hasattr(self, 'temp_unit') and self.temp_unit == 'F'):
temp = "%0.2f °F " % (temp * 9.0 / 5.0 + 32)
else:
temp = "%0.2f °C " % (temp)

return temp

def render(self):
self.display.prepare()

ipv4 = self.utils.get_ip()
core_stats = HassioUtils().hassos_get_info('core/stats', self.config)
cpu = core_stats["data"]['cpu_percent']
temp = self.get_temp()
mem = Utils.shell_cmd("free -m | awk 'NR==2{printf \"Mem: %s/%sMB %.2f%%\", $3,$2,$3*100/$2 }'")
storage = Utils.shell_cmd("df -h | awk '$NF==\"/\"{printf \"Disk: %d/%dGB %s\", $3,$2,$5}'")

if(self.display.icon_stats == 'text'):
self.display.draw.text((0, 0), "IP: " + ipv4, font=self.font(16), fill=255)
self.display.draw.text((0, 16), "CPU: " + str(cpu) + "LA", font=self.font(16), fill=255)
self.display.draw.text((80, 16), temp, font=self.font(16), fill=255)
self.display.draw.text((0, 32), mem, font=self.font(16), fill=255)
self.display.draw.text((0, 48), storage, font=self.font(16), fill=255)
else:
storage = Utils.shell_cmd("df -h | awk '$NF==\"/\"{printf \"%d/%dGB\", $3,$2}'")
mem = Utils.shell_cmd("free -m | awk 'NR==2{printf \"%.2f%%\", $3*100/$2 }'")
self.display.draw.text((0, 3), chr(62609), font=self.font(18, is_bold=False, is_icon=True), fill=255)
self.display.draw.text((65, 3), chr(62776), font=self.font(18, is_bold=False, is_icon=True), fill=255)
self.display.draw.text((0, 23), chr(63426), font=self.font(18, is_bold=False, is_icon=True), fill=255)
self.display.draw.text((65, 23), chr(62171), font=self.font(18, is_bold=False, is_icon=True), fill=255)
self.display.draw.text((0, 43), chr(61931), font=self.font(18, is_bold=False, is_icon=True), fill=255)
self.display.draw.text((19, 3), str(temp), font=self.font(16), fill=255)
self.display.draw.text((87, 3), str(mem), font=self.font(16), fill=255)
self.display.draw.text((19, 23), str(storage), font=self.font(16), fill=255)
self.display.draw.text((87, 23), str(cpu) + "LA", font=self.font(16), fill=255)
self.display.draw.text((19, 43), ipv4, font=self.font(16), fill=255)

self.display.show()
time.sleep(self.duration)
13 changes: 9 additions & 4 deletions bin/Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,18 @@ def slugify(text):

class HassioUtils(Utils):
@staticmethod
def hassos_get_info(type):
def hassos_get_info(type, config = None):
url = 'http://supervisor/{}'.format(type)
Utils.logger.info("Requesting data from '" + url + "'")
cmd = 'curl -sSL -H "Authorization: Bearer $SUPERVISOR_TOKEN" -H "Content-Type: application/json" ' + url
token = '$SUPERVISOR_TOKEN'
if config is not None:
if config.has_option('supervizor_token'):
token = config.get_option_value('supervizor_token')
cmd = 'curl -sSL -H "Authorization: Bearer ' + token +'" -H "content-type: application/json" ' + url
info = Utils.shell_cmd(cmd)
return json.loads(info)


@staticmethod
def get_hostname(opt = ""):
host_info = HassioUtils.hassos_get_info('host/info')
Expand All @@ -98,8 +103,8 @@ def compile_text(text, additional_replacements = {}):
"{ip}": lambda prop: HassioUtils.get_ip()
}
text = Utils.compile_text(text, {**replacements, **additional_replacements})
regex = re.compile("{hassio\.[a-z]+\.[a-z\.]+}")
return regex.sub(lambda match: HassioUtils.get_hassio_info_property(match.string[match.start():match.end()][len("hassio\."):-1]), text)
regex = re.compile("{hassio\\.[a-z]+\\.[a-z\\.]+}")
return regex.sub(lambda match: HassioUtils.get_hassio_info_property(match.string[match.start():match.end()][len("hassio\\."):-1]), text)

@staticmethod
def get_hassio_info_property(properties_string):
Expand Down
Empty file modified fonts/DejaVuSans-Bold.ttf
100644 → 100755
Empty file.
Empty file modified fonts/DejaVuSans.ttf
100644 → 100755
Empty file.
Binary file added fonts/PixelOperator.ttf
Binary file not shown.
Binary file added fonts/lineawesome-webfont.ttf
Binary file not shown.
Empty file modified img/cpu-64-bit.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/cpu.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/database-outline.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/database.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/disc.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/examples/cpu.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/examples/memory.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/examples/network.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/examples/splash.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/examples/static_goodbye.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/examples/stats.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/examples/storage.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/examples/welcome.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/harddisk.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/home-assistant-logo.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/ip-network.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified img/ram.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified oled.service
100644 → 100755
Empty file.
Loading