MacBook Pro Charge Limiting for Battery Health

Since working from home, my laptop is constantly left plugged into the mains as there isn’t much of a reason to ever disconnect, especially when you have a nice office to work in. I’ve been told leaving your laptop on charge has a negative impact on the longevity of your battery.

I’ve learnt this the hard way. The battery from my previous laptop, a Macbook Pro 2015, died a slow death until it got to a point where it soon became a glorified workstation. This seemed to happen quicker than I would have liked - within 3 years from purchase. Not something I’d expect from the build quality expected from an Apple product.

I was brave enough to replace the battery myself giving a new lease of life! The post teaser image is proof of my efforts. That picture was taken in when I managed to carefully pry the first cells of the old battery away from the existing adhesive. This was the most hardest part of the whole process!

My old laptop has now been replaced with the most recent iteration of the Macbook Pro, as I needed a little more power and most importantly 32GB of RAM to run intensive virtual environments. I made the conscious decision to actively take care of the battery and not repeat the mistakes I made in how I used my previous laptop. This is easier said than done especially when my laptop is connected via Thunderbolt to my monitor, both powering my laptop and gives dual-screen capability. It’s impossible to disconnect!

My only option was to find a “battery charge limiter” application that would set a maximum battery charge. Now, there is a great debate across forums whether going to such lengths does have any positive impact on battery health. Apparently, MacOS’s battery health management should suffice for the majority of scenarios when it comes to general usage. Going by experience, this didn’t help the lifespan of my previous Macbook’s battery. Hence my scepticism.

One indirect benefit of setting a charge limit is there will be less charge cycles counted, resulting in increased resale value should you decide to sell your laptop. Also, according to the Battery University, setting a charging threshold to 80% might get you around 1500 charge cycles.

If the likes of Lenovo, Samsung and Sony (all running on Windows) provide support software to limit the charge threshold, there has to be some substance to this approach. Unfortunately, you’re very limited to find a similar official application for macOS. But all is not lost. Two open-source variants carry out the job satisfactorily:

Both these apps modify the “Battery Charge Level Max” (BCLM) parameter in the SMC, which when set limit the charge level. The only thing to be aware of when using these applications is that sometimes the set charge limit can be wiped after a shutdown or restart. This is a minor annoyance I can live with. Out of the two, my preference was AlDente as I noticed the set charge limit didn’t get wiped as often when compared with Charge Limiter.

I’ll end this post with one final link from The Battery University on the best conditions to charge any battery - How To Charge and When To Charge.

My Work from Home Setup

It'll soon be coming up to a year working from home full-time due to the pandemic and I thought I'd write a post about my current setup as it has evolved over the months. Starting from a bare empty room with just a desk and chair has now become a fitting place to ensure maximum productivity and comfort.

I believe investing in a good home office setup is what can make working from home that little bit easier. Not everyone will be fortunate enough to have a single room dedicated to an office space, or afford all the niceties you've see other bloggers write about or showcased on Instagram.

The most important part of any office is investing in a good desk and chair. Everything else is secondary. I can't stress how important this is. Working on something like a dining table can get uncomfortable very easily and this can be a big distraction in itself. Start small with the basic's and overtime work your way up and make improvements when you can. This is the approach I’ve taken.

In general, working from home over long periods can be a real chore and a good setup will help you stay healthier and focussed whilst working. Interesting enough, The Atlantic wrote an article detailing why so many people are now experiencing medical problems after making the switch to working from home. A combination of long working hours, fewer breaks, stress and isolation is creating a negative impact on all of us.

Desk

I’m quite particular about desks and prefer ones that are a little industrial looking and made from real material. None of that MDF or veneered manufactured stuff. I went for a desk made from Indian reclaimed mango wood, constructed on a sturdy metal steel frame. It certainly adds a bit of character to the office.

I’ve been told I should have opted for a standup desk for further health benefits, but I’m doing just fine as both my desk and chair are at the right height suitable for my posture.

Chair

I went for an Ikea Alefjall office chair that provides great support in a relatively small form factor. The seat and backrest are height adjustable. You also get support for your thighs and back through its depth adjustment along with tilt capability.

Monitor

Samsung Ultrwide 34 inch monitor

I managed to snap a real bargain on an ultra-wide curved monitor from last years Amazon Black Friday deal and now a proud owner of a Samsung 34 inch ultra-wide beauty! This is a major upgrade over my Dell Ultrasharp, which by no means is a bad monitor, but just felt I needed more screen real-estate.

Being Thunderbolt-compatible is a bonus as my MacBook Pro can charge and transmit data simultaneously over a single cable. Makes cable management that little bit easier.

Mouse

I have a Logitech MX Master and it’s the most comfortable mouse I’ve ever used. Fits very comfortably in the palm of your hand and is very customisable. I don’t generally like wireless mice as they can be fiddly to connect and I always question the usage time in between charges.

This mouse works for weeks and that's with me leaving it switched on all the time. When it comes to charging, just connect the cable and carry on using it.

Keyboard

I've been a big fan of mechanical keyboards and prefer them over Apple’s over-priced ones. You just can’t beat the nice responsive “clickity-clack from every keypress. I’m still using the Ducky DK9008 Shine 2 my Dad got me in 2013. It’s still going strong unlike the many Apple keyboards that have failed previously.

Just be careful whilst using it when on a Zoom call. You will notice how noisy it can come across. The amount of noise emitted by a mechanical keyboard depends on the type of switches used. You can get some really good mechanical keyboards across a variety of price points. If I didn’t already have one, I’d choose one from the range offered by Keychron.

Speaker

I have a Google Home Max smart speaker that packs a real punch sat in the corner of the room. Even though the speaker itself isn’t in close proximity to where my desk is, I can summon commands without having to raise my voice.

Google Home Max speaker

Plants

An office space can quite quickly look very sterile and I like a little bit of greenery, which is thought to improve productivity and relieve stress. I’m not sure if that’s true. All I know it makes my working space that little bit nicer to be in. The plants I went for are very low maintenance and consist of:

  • Sansevieria: Known as “The Mother in Law's tongue” due it’s sharp upright leaves. It emits oxygen and filters toxins from the air.
  • Succulents: Really cheap and small enough to fit into any space.
  • Orchid: Not so low maintenance. Looks very cool when alive though! Mine is currently making its way back from the dead.

What's Next?

I think I'm done for the moment. It'll be nice to get some LED strips to fix to my desk and behind my monitor for subtle accent lighting.

Passing Search A Query Without Using The SearchBox Component In Algolia

You probably haven't noticed (and you'd be forgiven if this is the case!) that my site now has the ability to search through posts. This is a strange turn of events for me as I decided to remove search capability from my site many years ago as I didn't feel it added any benefits for the user. This became evident from Google Analytics stats where searches never hit high enough numbers to warrant having it. The numbers don't lie!

So what caused this turnaround?

I've noticed that I'm regularly referring back through posts to refresh myself on things I've done in the past and to find solutions to issues I know I've previously written about. Having a search would make trawling through my few hundred posts a lot easier. So this is more of a personal requirement than commercial. But there is an exciting aspect to this as well - experimenting with Algolia. Using Algolia search is something I've been meaning to look into for a long time and integrating with GatbsyJS.

The thought of having the good ol' magnifying glass back in the navigation makes me nostalgic!

Note: In this post, I won't be covering the basic Algolia setup or the plugins needed to install as there is already a great wealth of information online. Check out my "Useful Links" section at the end of the post.

Basic Setup

Integrating Algolia into GatbsyJS was relatively straight-forward due to the wealth of information that others have already written and also the plugins themselves. The plugins make light work of rendering search results quickly allowing enough customisations to the HTML markup for easy implementation within any site. By default, the plugins contain the following components:

  • InstantSearch
  • SearchBox
  • Hits
import algoliasearch from 'algoliasearch/lite';
import PropTypes from 'prop-types';
import { Link } from 'gatsby';
import { InstantSearch, Hits, Highlight, SearchBox } from 'react-instantsearch-dom';
import React from 'react';

// Get API keys from the environment file.
const appId = process.env.GATSBY_ALGOLIA_APP_ID;
const searchKey = process.env.GATSBY_ALGOLIA_SEARCH_KEY;
const searchClient = algoliasearch(appId, searchKey);

const SearchPage = () => (
  <InstantSearch
    searchClient={searchClient}
    indexName={process.env.GATSBY_ALGOLIA_INDEX_NAME}
  >
    <SearchBox />
    <Hits hitComponent={Hit} />
  </InstantSearch>
);

function Hit(props) {
  return (
    <article className="hentry post">
      <h3 className="entry-title">
        <Link to={props.hit.fields.slug}>
          <Highlight attribute="title" hit={props.hit} tagName="mark" />
        </Link>
      </h3>
      <div className="entry-meta">
        <span className="read-time">{props.hit.fields.readingTime.text}</span>
      </div>
      <p className="entry-content">
        <Highlight hit={props.hit} attribute="summary" tagName="mark" />
      </p>
    </article>
  );
}

Hit.propTypes = {
  hit: PropTypes.object.isRequired,
};

export default SearchPage;

The InstantSearch is the core component that directly interacts with Algolia's API and takes in two properties, "searchClient" and "indexName" containing the Application ID and Search Key that is acquired from the Algolia account setup. This component contains two child components, SearchBox is the search textbox and Hits that displays results from the search query.

It is the Hits component where we can customise the HTML with our own markup by using it's "hitComponent" attribute. In my case, I created a function to generate HTML where I access the properties from the search index. What's really cool is here is we have the ability to also highlight our search term where they may occur in the results by using the Highlight component (also provided by the Algolia plugin) and adding a "tagName" attribute.

Removing The SearchBox Component

The standard implementation may not suit all scenarios as you may want a search term to be sent to the InstantSearch component differently. For example, it could be from a custom search textbox or (as in my case) read from a query-string parameter. It wasn't until I started delving further into the standard setup I realised you cannot just remove the SearchBox component and pass a value directly, but there is a workaround.

I have expanded upon the code-snippet, above, to demonstrate how my search page works...

import algoliasearch from 'algoliasearch/lite';
import PropTypes from 'prop-types';
import { Link } from 'gatsby';
import { InstantSearch, Hits, Highlight, connectSearchBox } from 'react-instantsearch-dom';
import Layout from "../components/global/layout";
import React, { Component } from "react";

// Get API keys from the environment file.
const appId = process.env.GATSBY_ALGOLIA_APP_ID;
const searchKey = process.env.GATSBY_ALGOLIA_SEARCH_KEY;
const searchClient = algoliasearch(appId, searchKey);
const VirtualSearchBox = connectSearchBox(() => <span />);

class SearchPage extends Component { 
  state = {
    searchState: {
      query: '',
    },
  };

  componentDidMount() {   
    // Get "term" query string parameter value.
    let search = window.location.search;
    let params = new URLSearchParams(search);
    let searchTerm = params.get("term");

    // Send the query string value to a "searchState" object used by Algolia.
    this.setState(state => ({
      searchState: {
        ...state.searchState,
        query: searchTerm,
      },
    }));
 }

  render() {
      // Default "instantSearch" HTML to prompt user to enter a search term.
      var instantSearch = null;
      
      // If there is a search term, utilise Algolia's instant search.
      if (this.state.searchState.query) {
        instantSearch = <div className="entry-content">
                          <h2>You've searched for "{this.state.searchState.query}".</h2>
                          <div className="post-list archives-list">
                          <InstantSearch
                              searchClient={searchClient}
                              indexName={process.env.GATSBY_ALGOLIA_INDEX_NAME}
                              searchState={this.state.searchState}
                            >
                              <VirtualSearchBox />
                              <Hits hitComponent={Hit} />
                            </InstantSearch>  
                          </div>
                        </div>
      }
      else {
        instantSearch = <div className="entry-content">
                          <h2>You haven't entered a search term.</h2>
                          <p>Carry out a search by clicking the <em>magnifying glass</em> in the navigation.</p>
                        </div>
      }

      return (
        <Layout>
          <header className="page-header">
            <h1>Search</h1>
            <p>Search the knowledge-base...</p>
          </header>
          <div id="primary" className="content-area">
            <div id="content" className="site-content" role="main">
                <div className="layout-fixed">
                    <article className="page hentry">
                      {instantSearch}
                    </article>
                </div>
            </div>
          </div>
      </Layout>
    )
  }
}

function Hit(props) {
  return (
    <article className="hentry post">
      <h3 className="entry-title">
        <Link to={props.hit.fields.slug}>
          <Highlight attribute="title" hit={props.hit} tagName="mark" />
        </Link>
      </h3>
      <div className="entry-meta">
        <span className="read-time">{props.hit.fields.readingTime.text}</span>
      </div>
      <p className="entry-content">
        <Highlight hit={props.hit} attribute="summary" tagName="mark" />
      </p>
    </article>
  );
}

Hit.propTypes = {
  hit: PropTypes.object.isRequired,
};

export default SearchPage

My code is reading from a query-string value and passing that to a "searchState". The searchState object is created by React InstantSearch internally. Every widget inside the library has its own way of updating it. It contains parameters on the type of search that should be performed, such as query, sorting and pagination, to name a few. All we're interested in doing is updating the query parameter of this object with our search term.

If the query parameter from the "searchState" object is empty, render search results, otherwise, display a message stating a search term is required.

One thing to notice is the SearchBox has been replaced with a VirtualSearchBox, which uses the connector of the search box to create a virtual widget - in our case an empty span tag. This will link the InstantSearch component with the query. Having some form of search box component is compulsory.

Conclusion

I prefer not to use the out-of-the-box search box component as I can potentially save requests to Algolia's API, as searches aren't being made on the fly as a user enters a search term. This is the plugins default behaviour.

Passing a search term through a query-string may come across as a little backwards, especially when it's rather nice to see search results change before your eyes as you type letter-by-letter. However, this approach misses one key element: Tracking in Google Analytics. Even though I will be primary the person making the most use of my site search, it'll be interesting to see who else uses it and what search keywords are used.

Useful Links

ASP.NET Core: Using Assembly Build Date For Cache Busting

ASP.NET Core contains a variety of useful Tag Helpers to enable server-side code to participate in creating and rendering HTML elements in our Views. One Tag Helper, in particular, has the ability to cache bust links to static resources such as Image, CSS and JavaScript by appending an asp-append-version="true" attribute.

The asp-append-version attribute automatically adds a version number to the file name using a SHA256 hashing algorithm, so whenever the file is updated, the server generates a new unique version. For a deeper understanding on how ASP.NET Core performs this piece of functionality, give the following StackOverflow post a read: How does javascript version (asp-append-version) work in ASP.NET Core MVC?.

This approach works perfectly if you're linking to your static resources using the relevant HTML tag, for example img, script or link. In my scenario, I'm using a JavaScript library called LabJS - a dynamic script loader that gives the ability to control the loading and execution of different plugins. For example:

<script>
  $LAB
  .script("http://remote.tld/jquery.js").wait()
  .script("/local/plugin1.jquery.js")
  .script("/local/plugin2.jquery.js").wait()
  .script("/local/init.js").wait(function(){
      initMyPage();
  });
</script>

I need to be able to append a query string parameter to one of the JavaScript file references. One thing that came to mind was to use the applications last build-time as the cache busting value. Whenever the application is updated, this value will automatically be updated so no manual intervention is required.

I found code examples from meziantou.net that demonstrated various approaches to acquiring an applications build date. I modified the "Linker timestamp" example to return a Unix timestamp in a newly created class called AssemblyUtils.

public class AssemblyUtils
{
    #region Properties

    public int UnixTimestamp { get; set; }

    #endregion

    /// <summary>
    /// Get timestamp in Unix seconds for the last build.
    /// </summary>
    /// <returns></returns>
    public static int GetBuildTimestamp()
    {
        const int peHeaderOffset = 60;
        const int timestampOffset = 8;

        byte[] bytes = new byte[2048];

        using (FileStream file = new FileStream(Assembly.GetExecutingAssembly().Location, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            file.Read(bytes, 0, bytes.Length);

        int headerPos = BitConverter.ToInt32(bytes, peHeaderOffset);
        int unixTime = BitConverter.ToInt32(bytes, headerPos + timestampOffset);

        return unixTime;
    }
}

The code will only return the Assembly information if your Visual Studio .csproj file (from version 15.4 onwards) includes the following setting within the <PropertyGroup> settings:

<Deterministic>False</Deterministic>

It would be a waste to constantly call the GetBuildTimestamp() method to acquire assembly information directly within the page View, when the most ideal approach would be to make this call once on application startup.

public void ConfigureServices(IServiceCollection services)
{
    #region Assembly Utils - Build Time

    Action<AssemblyUtils> assemblyBuildOptions = (opt =>
    {
        opt.UnixTimestamp = AssemblyUtils.GetBuildTimestamp();
    });

    services.Configure(assemblyBuildOptions);
    services.AddSingleton(resolver => resolver.GetRequiredService<IOptions<AssemblyUtils>>().Value);

    #endregion
}

We can access the build timestamp value using Dependency Injection within a base controller that gets inherited by all controllers.

public class BaseController : Controller
{
    private int _buildTimetamp { get; set; }

    public BaseController(AssemblyUtils assemblyUtls)
    {
        _buildTimetamp = assemblyUtls.UnixTimestamp;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        base.OnActionExecuting(context);

        // Assign build timestamp to a View Bag.
        ViewBag.CacheBustingValue = _buildTimetamp;
    }
}

The timestamp is assigned to a ViewBag that can then be accessed at View level.

<script>
  $LAB
  .script("http://remote.tld/jquery.js").wait()
  .script("/local/plugin1.jquery.js")
  .script("/local/plugin2.jquery.js").wait()
  .script("/local/init.js?v=@ViewBag.CacheBustingValue").wait(function(){
      initMyPage();
  });
</script>

This will result in the following output:

<script>
  $LAB
  .script("http://remote.tld/jquery.js").wait()
  .script("/local/plugin1.jquery.js")
  .script("/local/plugin2.jquery.js").wait()
  .script("/local/init.js?v=1609610821").wait(function(){
      initMyPage();
  });
</script>