feat: Initial commit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
---
|
||||
tags: formulas, HVAC, tech-tip
|
||||
---
|
||||
# Calculate SEER Degradation by Age
|
||||
|
||||
This is a quick tech-tip to learn how to calculate the degradation of SEER based
|
||||
on age.
|
||||
|
||||
The degradation of SEER is due to fouling of the evaporator coil with dirt and
|
||||
refrigerant charge losses. It should be noted that this is not true for all
|
||||
applications, but is used as an estimation based on research done by the `DOE`
|
||||
of the average degradation based on systems tested.
|
||||
|
||||
## Formula
|
||||
|
||||
This is the formula used to calculate the SEER based on age of the evaporator
|
||||
coil / air handler.
|
||||
|
||||
$$ SEER_d = SEER_n \times (1 - M)^{age} $$
|
||||
|
||||
| Where | |
|
||||
| -------- | ------------------------------------------ | --- |
|
||||
| $SEER_d$ | Degradated SEER rating |
|
||||
| $SEER_n$ | Nominal SEER rating when equipment was new | |
|
||||
| M | Maintenance factor, 0.01-0.03 |
|
||||
| age | The age of the equipment, in years |
|
||||
|
||||
The maintenance factor of 0.01 is for expertly maintained equipment and 0.03 is
|
||||
for unmaintained. The maintenance factor in essence is based on 1%-3%
|
||||
degradation per year, however there are some
|
||||
[studies](https://publications.energyresearch.ucf.edu/wp-content/uploads/2018/09/FSEC-PF-474-18.pdf)
|
||||
that show that this can actually be as high as 5% or above depending on climate.
|
||||
We could use up to 0.05 as the maintenance factor, just to see what the "range"
|
||||
of degradation would be.
|
||||
|
||||
Interestingly, the study linked also shows that the degradation is higher the
|
||||
higher the tonnage of the equipment. It also shows that the degradation is lower
|
||||
per year the higher the nominal SEER rating of the system (which is corelated to
|
||||
using TXV's and lower airflow rates because of the equipment having multiple
|
||||
stages).
|
||||
|
||||
## Example
|
||||
|
||||
Let's consider that we have a 13 SEER piece of equipment that was matched when
|
||||
installed and the system is 15 years old.
|
||||
|
||||
Plugging those numbers into our formula.
|
||||
|
||||
---
|
||||
|
||||
#### Lowest Range (1% degradation / year)
|
||||
|
||||
$$ SEER_d = 13 \times (1 - 0.01)^{15} = 11.2 $$
|
||||
|
||||
---
|
||||
|
||||
#### Highest Rage (5% degradation / year)
|
||||
|
||||
$$ SEER_d = 13 \times (1 - 0.05)^{15} = 6 $$
|
||||
|
||||
---
|
||||
|
||||
An expertly maintained system may not have degraded that much, with an 11.2 SEER
|
||||
vs. a poorly maintained / dirty system that also suffers from refrigerant charge
|
||||
losses can be as low as 6 SEER.
|
||||
|
||||
Thanks for learning how to estimate SEER degradation based on equipment age!
|
||||
142
content/articles/2023-10-18-the-struggle.md
Normal file
142
content/articles/2023-10-18-the-struggle.md
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
public: true
|
||||
date: 2023-10-18
|
||||
tags: article, general
|
||||
banner: true
|
||||
---
|
||||
|
||||
# The Struggle
|
||||
|
||||
This is a get stuff of my chest article. Probably going to be a mixture of ranting and whining. If
|
||||
that is not your cup of tea then feel free to skip this article.
|
||||
|
||||
## The struggle
|
||||
|
||||
It has been one of those "when it rains, it pours" type of weeks. As write this, I feel like a baby
|
||||
/ complainer, which is not my intention, but here it goes.
|
||||
|
||||
My aunt had a stroke and is likely not going to make it through the week. My aunt has always lived
|
||||
in Florida during my life / memory, but has always come to visit. She is my mom's oldest sister (9
|
||||
years apart), so she helped raise my mother growing up. They've always had a close relationship and
|
||||
are like two peas in a pod. They would always visit us, especially while my grand-parents were still
|
||||
alive. She is currently not really awake or expected to make it. She does not eat / drink and
|
||||
they've basically just been making her comfortable.
|
||||
|
||||
One of my employees broke his neck over the weekend. I don't have a ton of details on this subject,
|
||||
but he had surgery to fuse some discs back together and should eventually be ok, however it will be
|
||||
a long recovery time.
|
||||
|
||||
A few weeks ago, I commited one of the deadly sins of running a blower door test without checking /
|
||||
asking about the fireplaces, which caused lord knows how much damage from the soot that was spread
|
||||
throughout the living room(s).
|
||||
|
||||
An online friend lost her mother.
|
||||
|
||||
All of these things have just got me emotional.
|
||||
|
||||
## My secrets
|
||||
|
||||
I am an emotional person, sometimes to my detriment. I'm prone to fits of anger and depression. I
|
||||
spent many years masking things through alcohol abuse and many other idiotic tendencies. I still
|
||||
drink on occasion, but not as I once did.
|
||||
|
||||
I often put my faith in folks, even if I haven't met them in person, this can often lead to being
|
||||
let down. I guess I struggle sometimes with the reality of online relationships. I think that people
|
||||
are genuine, because I'm that way (how I act online is the same as I act in person for the most
|
||||
part). I will happily show / share the good and the bad, in hopes that someone may learn. This is
|
||||
not at all something to be ashamed of, or anything that I'd like to change, however it does on
|
||||
occasion lead me into turmoil when I find out some true identity / personality of someone.
|
||||
|
||||
I hold grudges, like forever grudges. It is very hard for me to get over somethings, even if I want
|
||||
to. I can sometimes put things out of my mind, but when I'm spinning, they will resurface. I want to
|
||||
forgive people, but it is just not the way it works for me often times. It takes a lot to get me
|
||||
beyond my breaking point, but once someone has gotten me there, there is likely no turning back for
|
||||
that person. Luckily, I can count on one hand the number of folks that I've had to cut out of my
|
||||
life for these reasons. I'd love to say that I'm a bigger / better person, that I forgive them, but
|
||||
truthfully I do not.
|
||||
|
||||
My emotions sometimes make me say things publicly that I should keep to myself. I don't have a lot
|
||||
of regret around these things, because I generally mean what I say, and also feel we are entitled to
|
||||
opinions as well as entitled to change those opinions as often as we see fit. Although I don't
|
||||
regret them per-se, it does make me wonder what type of impression it leaves (something for future
|
||||
pondering, perhaps).
|
||||
|
||||
I'm one of those that says "I don't care what they think about me", when I really do care what
|
||||
people think about me. This often leads to fear and anxiety, especially in social settings. I'm not
|
||||
the greatest at conversation, I'm better at writing or some interaction that gives me a bit of time
|
||||
to think before responding. Therefore, I'm quick to make a joke or something rather than having
|
||||
thoughtful / genuine responses.
|
||||
|
||||
## Community
|
||||
|
||||
All of these things have just got me thinking about community. Community is an aspect that seems
|
||||
distant nowadays, even though we have more opportunity than ever. We call ourselves part of
|
||||
communities, especially online, but I'm not convinced that it is really community (or at least not
|
||||
most of the time, not saying it can't be / in absolute terms).
|
||||
|
||||
I have met a lot of folks online that I consider true friends. Friends that I would do anything for
|
||||
and feel they would do anything for me, just like my real life friends who have stuck with me for
|
||||
many years. These are people that challenge me and how I think on a regular basis. They do their
|
||||
best to lift me up when I'm down.
|
||||
|
||||
It's easy to hide when so much of our interactions are not really in person, to feel like the
|
||||
relationships we do have are not genuine, but that's awfully cynical. I prefer to give people the
|
||||
benefit / trust they deserve until, at least until they don't then see my part about holding grudges
|
||||
;)
|
||||
|
||||
Do you ever feel the same? Instead of being cynical and down, today, I'm trying to be positive. I'm
|
||||
trying to share a little bit of what I feel community should be. Although, I've never met HVAChicks
|
||||
Jennifer in real life, I read today about her mom and decided to take an hour or so to put together
|
||||
a website for the [HVAChicks Community](https://hvachicks.com), hoping it would bring a moment of
|
||||
happiness to her day. While I've deployed several websites in the past, this one for some reason was
|
||||
a total PITA, but I got through it. I could feel how grateful she was when I shared it with her (and
|
||||
it's basic AF), but that truly made me feel useful / great after being down in the dumps for a bit.
|
||||
|
||||
## Conclusion
|
||||
|
||||
I've probably rambled enough and am losing direction here, so I will end with a few things.
|
||||
|
||||
1. Build the community around you that you want / deserve.
|
||||
1. Don't be so quick to pass judgment.
|
||||
1. Spend time with your loved ones, while you can.
|
||||
1. Tell someone you love them.
|
||||
|
||||
Finally, I'd like to shout out some people that I'm grateful for. This is non-exhaustive list, if I
|
||||
left you off, I'm sorry and please do not take it personally.
|
||||
|
||||
1. Bryan Orr
|
||||
- Bryan has cultivated an awesome group / community that I'm proud to be a part of. His time and
|
||||
commitment to the HVAC industry is something that I hope he himself is proud of. Without Bryan,
|
||||
mostly all the following names would not even be in my vocabulary.
|
||||
1. Ty Branaman (forget exactly how to spell his last name).
|
||||
- Ty has such a great personality during his videos / training and I'm sure it's a struggle to be
|
||||
positive all the time, however it brings me hope and joy everytime!
|
||||
1. Dustin (mother f'n) Cole
|
||||
- Dustin is like a brother that I never had, he's a true master of his craft and I know that I
|
||||
can rely on him anytime I need it!
|
||||
1. Genry Garcia
|
||||
- The cuban crusader who has taught me so much about home performance, and especially ZPD (baby,
|
||||
baby)!
|
||||
1. Chris Hughes
|
||||
- It's hard to put into words here, but I know that Chris is there to talk when I need it and
|
||||
he's a great motivator because of his action items :)
|
||||
1. Eric Kaiser
|
||||
- I think genuine when I think of Eric, I hear his voice in my head when I write reports because
|
||||
of guidance he has given in the past. He truly wants to help teach people.
|
||||
1. Michael Cianfracco (??)
|
||||
- Man does he make me laugh and we share a passion for the 4-Roses.
|
||||
1. HVAChicks Jennifer (I'll butcher her last name from memory)
|
||||
- Jennifer is inspiring with her goals and everything she does to help anyone in the industry. I
|
||||
can't wait to meet her in person one day soon!
|
||||
1. HVAC Overtime Crew
|
||||
- I'm lumping these guys together because I truly enjoy when I'm able to make their live stream
|
||||
on Friday's. And I interact with A-Team a lot and really appreciate him / them!
|
||||
1. Alicia Hollon
|
||||
- Alicia is so awesome to me, I mean she just made my favorite pancakes and sausage for dinner,
|
||||
love you babe!
|
||||
|
||||
Lastly, I have to shout out to my mom. I know she's going through a rough time with my aunt
|
||||
currently. My mother is the kindest gentlest soul that I've ever met. I look up to all 5' of her!
|
||||
|
||||
At any rate, thanks for sticking it out to the end through my ramblings. Be kind to someone today
|
||||
and tomorrow!
|
||||
56
content/articles/2023-10-21-you-should-learn-markdown.md
Normal file
56
content/articles/2023-10-21-you-should-learn-markdown.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
tags: HVAC, general, programming
|
||||
---
|
||||
|
||||
# You Should Learn Markdown
|
||||
|
||||
This is a quick article about why you should learn markdown.
|
||||
|
||||
## What is markdown
|
||||
|
||||
Markdown is a "mark-up" language. It allows you to write content in plain text that can be easily
|
||||
converted to other formats, such as **html, pdf, docx**, and many more. All the articles written on
|
||||
this website are written in markdown, here's an image of this article written in markdown.
|
||||
|
||||

|
||||
|
||||
The reason you should learn markdown is that it allows you to focus on the content / text of your
|
||||
content with simple concepts for formatting. Markdown is used heavily in documenting software
|
||||
projects, which is how I got introduced to it, however it can scale all the way up to writing
|
||||
research papers or even books.
|
||||
|
||||
I use markdown for probably 80% of all the text documents I need to write, from company documents,
|
||||
web / software documentation, and so on. It allows me to get content out quickly without having to
|
||||
click around with formatting options in a program like **Word** or **Pages**. To be clear, a lot of
|
||||
the reports and things I generate for my **Home Performance Assessments** are written using
|
||||
**Pages** (for now at least) because I have templates that make the documents look more
|
||||
professional, however I am working on solutions to migrate those to be markdown based.
|
||||
|
||||
Markdown is supported in mostly all the **Google** tools, as well as the **Outlook** email client
|
||||
(which is one of my least favorite tools, BTW). It makes it simple to create tables, lists, and many
|
||||
common document related tasks.
|
||||
|
||||
## Resources
|
||||
|
||||
You can learn more about the syntax used for markdown at
|
||||
[markdownguide.org](https://www.markdownguide.org/basic-syntax/).
|
||||
|
||||
### A non-exhaustive list of where you can use Markdown and editors.
|
||||
|
||||
| Platform | Description |
|
||||
| ------------ | --------------------------------------------------------------- |
|
||||
| Google | Most goggle tools support markdown, docs, sheets, etc. |
|
||||
| Outlook | Although I hate outlook email client, it does support markdown. |
|
||||
| Dillinger.io | An online tool for writing / previewing markdown |
|
||||
| MarkText | A multi-platform editor for writing markdown |
|
||||
|
||||
You can use tools such as [pandoc](https://pandoc.org/#) to convert Markdown files to other formats.
|
||||
|
||||

|
||||
|
||||
## Conclusion
|
||||
|
||||
I hope that you take the time to research and see if Markdown is good fit for your document /
|
||||
content creation.
|
||||
|
||||
Thanks for reading until the end!
|
||||
92
content/articles/2023-10-27-heat-recovery-chiller.md
Normal file
92
content/articles/2023-10-27-heat-recovery-chiller.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
tags: HVAC, hydronics, chiller
|
||||
---
|
||||
|
||||
# Heat Recovery Chiller
|
||||
|
||||
This is an article that I wrote back in 2020, but I don't believe that I published it anywhere, so
|
||||
I'm doing it now. I did discuss this on
|
||||
[this episode of the HVAC School Podcast.](https://hvacrschool.com/podcasts/is-the-future-of-air-conditioning-self-contained-propane-chillers/)
|
||||
|
||||
## Heat Recovery Chillers
|
||||
|
||||
I have always had a love and passion for hydronic systems, perhaps it's because they are not that
|
||||
common in most areas. The designs tend to be elegant and the flexibility is unparalleled by most
|
||||
equipment choices currently available in the U.S. I'm going to try to not get too far into the weeds
|
||||
in this article, but offer an overview of what I feel would be my dream system.
|
||||
|
||||
## The Source
|
||||
|
||||
A heat recovery chiller will do simultaneous heating and cooling, as opposed to a traditional
|
||||
heat-pump or reverse cycle chiller that can only operate in one mode at a time. This system would
|
||||
have a second refrigerant to water heat exchanger and utilize the air-source when we don't need to
|
||||
bank / store heat, or are running in heat only mode. There are several benefits to this style
|
||||
system, the main being that while operating simultaneous heating / cooling mode the COP of the
|
||||
system doubles. For example, the [Multi-Aqua MHRC2](https://multiaqua.com/mhrc2/) shows a COP of
|
||||
about 8, which is approximately equivalent to 30 EER or 34 SEER. This gives us geothermal level
|
||||
performance without the need of a field.
|
||||
|
||||
## Indoor Portion
|
||||
|
||||
The indoor portion of the system, we would utilize buffer tanks as thermal storage for the chiller.
|
||||
The buffer tanks give us several benefits, including longer run times for the chiller, load
|
||||
matching, and the ability to size for larger load (heating or cooling) without some of the problems
|
||||
that occur when over-sizing a traditional system. The buffer tanks also allow sizing more
|
||||
aggressively by having storage during peak load conditions.
|
||||
|
||||

|
||||
|
||||
This shows a cooling buffer tank as well as a heating buffer tank. The horizontal pumps are what
|
||||
circulate water through the chiller, while the vertical pumps are what distribute the water to the
|
||||
load / emitters. This configuration allows for water to be used for the loads first and extra
|
||||
capacity to go into the buffer tanks. The buffer tanks also offer hydraulic separation for the pumps
|
||||
(meaning that they won't interfere with each other if / when there are different flow rates).
|
||||
Another advantage of the buffer tanks is that you can connect multiple heating or cooling sources in
|
||||
parallel to the system. This is shown by the extra tees in the hot buffer tank where we could
|
||||
connect something such as solar water collectors, pellet or wood boiler, or a conventional boiler.
|
||||
You'll also notice on the right side of the hot buffer tank, that I am showing a brazed plate heat
|
||||
exchanger that would be used to supply domestic hot water.
|
||||
|
||||
## Distribution System
|
||||
|
||||
This could be a number of things, from radiant panels, in-floor, high-output baseboard radiators,
|
||||
however I'm going to model it as a 4-pipe hydronic air handler.
|
||||
|
||||

|
||||
|
||||
The hydronic air handler is able to provide cooling, heating, and re-heat dehumidification, all in
|
||||
one package. With a hydronic system, it would be very easy to load match by controlling the fan
|
||||
speed and utilizing a Delta-T pump or outdoor reset controls. These air handlers are available from
|
||||
several manufacturers and come in different styles from traditional (as shown), small duct high
|
||||
velocity systems, and even ductless styles.
|
||||
|
||||
## Disadvantages
|
||||
|
||||
1. Not highly available in the U.S.
|
||||
1. When the chiller is down nothing works (not much different than traditional)
|
||||
1. Lack of understanding / technicians afraid to work on this style system.
|
||||
1. Potentially higher upfront costs.
|
||||
1. Lack of design or planning could cause unhappy clients (same with traditional)
|
||||
1. Distribution systems need to be designed around low water temperatures (not a drop in replacement
|
||||
for traditional boiler systems)
|
||||
|
||||
## Advantages
|
||||
|
||||
1. Central plant for heating, cooling, and DHW (also a disadvantage)
|
||||
1. Flexible distribution options / methods.
|
||||
1. Self contained refrigerant circuit
|
||||
1. Ability to produce / store thermal energy during off peak electrical hours
|
||||
1. Ability to move BTU's more efficiently through water than air
|
||||
1. Long life for distribution system (50-100+ years)
|
||||
1. Easier transition to natural / flammable refrigerants
|
||||
|
||||
## Conclusion
|
||||
|
||||
In conclusion, there are many advantages to this style system, as well as disadvantages. One of the
|
||||
main points is to think of the system in 3 distinct components, the source(s), thermal storage, and
|
||||
distribution. As mentioned, this only one concept, but in reality, there are many ways to accomplish
|
||||
this, which is one of it's advantages. For example, perhaps in certain scenarios it makes sense to
|
||||
dump excess heat into a pool, create ice storage during off peak hours, cascade into another water
|
||||
-> water heat pump for higher temperature distribution, incorporate solar collectors, and so on.
|
||||
|
||||
Thank you for reading all the way to the end!
|
||||
66
content/articles/2023-10-30-hope.md
Normal file
66
content/articles/2023-10-30-hope.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
tags: general
|
||||
---
|
||||
|
||||
# Hope
|
||||
|
||||
This is a piggy-back article off of
|
||||
[The Struggle](https://mhoush.com/posts/20231018224631-the-struggle/) article that I had recently.
|
||||
|
||||
I was a bit of a "Debbie Downer" in that article, which is okay, I get that way sometimes. In this
|
||||
article I'd like to take a minute to layout some of the things that give me hope, things that I
|
||||
focus on to try and kick myself out of the rut I can get into sometimes.
|
||||
|
||||
I believe it's important to have **hope**, to realize that feeling down is normal / part of being
|
||||
human. It's just as important to have hope. For some it is a harder thing to focus on, it may be
|
||||
easy for us to try and dwell in negative feelings, to some that may feel more comfortable. When
|
||||
we're feeling cynical, it's easy to feed the negativity, although it's rarely helpful.
|
||||
|
||||
## Hope
|
||||
|
||||
One of the things that I try to focus on is that I'm human, it brings me hope to realize that I'm
|
||||
not the only one who feels the way that I do. This was evident by the feedback on my previous
|
||||
article.
|
||||
|
||||
> My problems are very much "first world problems".
|
||||
|
||||
Make no mistake, I'm selfish, but it's the selfishness that makes me feel distant or disconnected.
|
||||
The times that I feel connected are without a doubt the times that I let go of my ego and focus on
|
||||
things outside of myself.
|
||||
|
||||
It's the feeling I get from going to customers home and feeling truly appreciated. There's no push
|
||||
back on price, no one telling me how I should run my business, no "the part only costs $x on
|
||||
amazon", etc.
|
||||
|
||||
The feeling of seeing your child smile. The safety you feel when getting a hug from your mother or
|
||||
father. When you're looking up at the stars and realize how small you actually are.
|
||||
|
||||
When you're out with friends and trick the DJ into playing a Mr. Bungle song and no one else
|
||||
appreciates it, but you. The times when your guard is completely down and you laugh with abandon.
|
||||
|
||||
The smell of your lover's hair when you're holding them close. The joy you receive from sitting with
|
||||
your pets. The songs of nature, insects, birds, etc. The sounds of waves / water in the distance.
|
||||
|
||||
The feeling when you've helped someone learn a new skill, when something finally clicks and makes
|
||||
sense. When a complex topic is understood at a fundamental level. The breakthrough of a problem
|
||||
you've pondered for days / months / years.
|
||||
|
||||
## Conclusion
|
||||
|
||||
There are so many things to be grateful for. This does not mean that there will not be hard times,
|
||||
there surely will be. This does not mean that you are wrong for having negative feelings, for
|
||||
feeling depressed or anxious. There are many around you that likely feel similar (they may just not
|
||||
be able to express it in the same way). You never truly know what the person next to you is dealing
|
||||
with. Be compassionate (including showing yourself compassion).
|
||||
|
||||
When you are feeling down, try to do something for someone else. Make your own list of things to be
|
||||
thankful for. Maybe my non-exhaustive list above will give you some ideas. Write those items down
|
||||
regularly so that you can look back on them when you feel out of balance.
|
||||
|
||||
There is plenty of negative energy in this world, be true and genuine and you will receive rewards.
|
||||
When you focus on blessings then the negativity will fade.
|
||||
|
||||
What impact are you going to leave with what little time we have?
|
||||
|
||||
I hope that you enjoyed this short article. Know that you are _NOT_ alone, you are important, and
|
||||
that I appreciate _you!_
|
||||
66
content/articles/2023-12-12-cancel-this.md
Normal file
66
content/articles/2023-12-12-cancel-this.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
tags: HVAC
|
||||
image: 2023-12-12-cancel-this.gif
|
||||
---
|
||||
|
||||
# Cancel This
|
||||
|
||||
This post is going to be hard to put into words, it's going to seem egotistical at points, but know
|
||||
that my actions / feelings online are the same as they are if you met me in person. The problem with
|
||||
online interactions is tone and other subtleties do not come across, so it's hard to tell when
|
||||
someone is joking or being serious.
|
||||
|
||||
## The Meat
|
||||
|
||||
It has been brought to my attention that someone out there thinks that I'm sexist and should not
|
||||
speak at the HVAC School symposium because of it. They are holding a curse word against me that I
|
||||
said two years ago, in the moment A) it was fitting and B) it was one of those times that my mouth
|
||||
was working faster than my brain (raise your hand if that's ever happened to you).
|
||||
|
||||
I am an open book, I am not afraid of showing failures and strengths. I've never claimed to be
|
||||
perfect (well, actually I have but it's always a joke). I'm not at all condoning what I've said in
|
||||
the past, nor will I repeat it out of context now, but I communicate with people like adults and I
|
||||
look at people as piers. We do not always have to agree with one another, conflict helps us learn
|
||||
where to grow, but we also can not expect people to change to fit our wants / needs, that's just not
|
||||
how it works.
|
||||
|
||||
I do not at all agree with the tactics used by whomever is behind this. I would happily discuss this
|
||||
in private or out in the open. You can find all my contact / social information in the sidebar of
|
||||
this website and I've offered up my cell phone number in private groups that we are all probably a
|
||||
part of.
|
||||
|
||||
## The Potatoes
|
||||
|
||||
I am a father of 3 girls, I have a sister, talk crap about my mom and you'll find out who I am! I
|
||||
love women, I have no problem with women in the trades, I encourage women who are in the trades. I
|
||||
try to help every person that I can, if I can. I have failed more times than you can imagine! I am
|
||||
self taught in about everything I do, so I give of it freely. I have all kinds of work to do to grow
|
||||
into the man that I should be, but at the same time, I am not afraid to be the man that I am today.
|
||||
I will greet you by whatever pronoun you want to be referred as.
|
||||
|
||||
I would love to say that I don't judge people, but I do, just as I'm being judged! I am fine with
|
||||
someone judging me and having opinions about me, I mean that is our right. I will not say that
|
||||
person is wrong, because in their mind they're right. What I will not do is stop being myself. I
|
||||
know for a fact that I've helped more people than this person has gotten "cancelled". I will
|
||||
continue to help people in the trades, homeowners, or people in need. I also will not remain quiet!
|
||||
I will give my opinion like it or not. There are people in my corner that really know me and my
|
||||
intentions.
|
||||
|
||||
## Final Words
|
||||
|
||||
The trades are rough around the edges, just as am I. The trades are a slow turning ship (just look
|
||||
at how many still do not follow proper practices, etc. even with the capabilities and resources
|
||||
available today).
|
||||
|
||||
If you are new / coming into the trades then I hope you are not overly sensitive to foul language
|
||||
and other inappropriate comments. Thick skin is valuable in the trades, but at the same time don't
|
||||
be afraid to speak up if something offends you. Remember we are adults, we can handle disagreements
|
||||
like adults (well, some of us can). Adults can learn from those of any age, they can reflect on
|
||||
their own behavior and make their own decisions.
|
||||
|
||||
I personally think that being offended is not a bad thing, it causes us to reflect / repair our
|
||||
foundational beliefs. I hope that we can stop with this "cancel culture" of today, stop hiding
|
||||
behind a keyboard, and be a diverse community.
|
||||
|
||||
To wrap things up, I just want to reiterate that I am not at all mad at whomever is behind this. To
|
||||
everyone out there who has shown me love and support, I greatly appreciate it.
|
||||
31
content/articles/2023-12-14-most-important-job.md
Normal file
31
content/articles/2023-12-14-most-important-job.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
image: 2023-12-14-most-important-job.gif
|
||||
tags: HVAC, tech-tip
|
||||
---
|
||||
|
||||
# Most Important Job
|
||||
|
||||
This short tech tip is about something one of my good friends in the trade told me a long time ago,
|
||||
that has stuck with me through the years. It is a simple phrase / mindset.
|
||||
|
||||
> "The current job you're on, is the most important job of your day".
|
||||
|
||||
This mindset should be carried from the dispatcher, manager, and technician to make this successful.
|
||||
I'm not gonna lie and say this is always easy, or even always possible, but if you strive to use
|
||||
this mindset then you will gain lifelong customers and satisfaction of not having loose ends hanging
|
||||
out there.
|
||||
|
||||
I do think it's also important to acknowledge that there are times that a technician has exhausted
|
||||
all their options, maybe they just don't have the mental capacity anymore to continue with a problem
|
||||
job, or need a break to come back another day with a fresh set of eyes, etc... This is also an
|
||||
important thing for a technician to realize, and hopefully those type of instances are much more few
|
||||
and far between, but I do want to acknowledge that scenario does also exist.
|
||||
|
||||
If you are a business owner, then this is something to consider instilling in your operations.
|
||||
Empower your technicians and dispatchers to understand when things need to be shuffled around in
|
||||
order to accommodate taking care of the customer at hand. It costs a lot of money to get a
|
||||
technician to job, so minimizing truck rolls to the same job is important from a business
|
||||
standpoint.
|
||||
|
||||
This was just a quick tech tip of something that has been rolling around in my mind lately. I hope
|
||||
you find it helpful and it sticks with you through your career as it has mine.
|
||||
71
content/articles/2024-02-27-elevating-hvac.md
Normal file
71
content/articles/2024-02-27-elevating-hvac.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
tags: HVAC
|
||||
---
|
||||
|
||||
# Elevating HVAC: A Skilled Trade Beyond Labor
|
||||
|
||||
This is a guest post from my good friend, **Ty Branaman**.
|
||||
|
||||
## Introduction:
|
||||
|
||||
Heating, Ventilation, and Air Conditioning (HVAC) is often mistakenly categorized as a simple labor
|
||||
job, overshadowing the intricate skill set and technical expertise required in this field. Labor
|
||||
jobs are also very important positions needed to keep society running. HVAC, like other skilled
|
||||
trades, demands a high level of knowledge, precision, and adaptability. This article aims to shed
|
||||
light on the misconception surrounding HVAC, emphasizing its status as a skilled trade that plays a
|
||||
pivotal role in creating comfortable and efficient living and working environments.
|
||||
|
||||
## Technical Proficiency:
|
||||
|
||||
One of the defining features that distinguish HVAC as a skilled trade is the level of technical
|
||||
proficiency required. HVAC professionals must have a deep understanding of thermodynamics, fluid
|
||||
mechanics, electrical circuits, Psychrometrics and air properties. This knowledge is essential for
|
||||
designing, installing, and maintaining HVAC systems that operate seamlessly and efficiently.
|
||||
|
||||
## System Design and Installation:
|
||||
|
||||
HVAC professionals are involved in the intricate process of designing and installing heating,
|
||||
ventilation, and air conditioning systems. This task requires not only a thorough understanding of
|
||||
the physical principles governing HVAC but also the ability to tailor solutions to meet the unique
|
||||
needs of each space. Proper system design and installation are crucial for achieving optimal energy
|
||||
efficiency and performance.
|
||||
|
||||
## Diagnostic Skills:
|
||||
|
||||
Troubleshooting and diagnosing issues in HVAC systems require a keen analytical mind and
|
||||
problem-solving skills. Skilled HVAC technicians possess the ability to identify and rectify
|
||||
problems efficiently, ensuring minimal downtime and disruption to the comfort of occupants. This
|
||||
diagnostic acumen is a hallmark of a trade that goes beyond routine labor.
|
||||
|
||||
## Adaptability to Advanced Technologies:
|
||||
|
||||
The HVAC industry is in a constant state of evolution, with new technologies and innovations
|
||||
continuously being introduced. Skilled HVAC professionals are adaptable and stay abreast of these
|
||||
advancements. From smart thermostats to energy-efficient systems, they integrate cutting-edge
|
||||
technologies to provide state-of-the-art solutions for their clients.
|
||||
|
||||
## Safety and Compliance:
|
||||
|
||||
Safety is paramount in the HVAC trade. Professionals must adhere to strict safety protocols to
|
||||
protect both themselves and the occupants of the spaces they work in. Additionally, compliance with
|
||||
industry regulations and codes is a testament to the skilled nature of the trade, ensuring that HVAC
|
||||
systems meet the highest standards of safety and efficiency.
|
||||
|
||||
## Continual Learning and Certification:
|
||||
|
||||
Unlike a labor job, HVAC professionals engage in continual learning to stay current with industry
|
||||
trends and technological advancements. Many pursue certifications and attend training programs to
|
||||
enhance their skills and expand their knowledge base. This commitment to ongoing education is a
|
||||
hallmark of skilled trades.
|
||||
|
||||
## Conclusion:
|
||||
|
||||
Heating, Ventilation, and Air Conditioning is undeniably a skilled trade that goes beyond the
|
||||
perception of a labor job. Labor jobs are also very important to the function of society and I have
|
||||
proudly done these jobs myself. The technical proficiency, problem-solving abilities, adaptability,
|
||||
and commitment to safety make HVAC professionals essential contributors to the creation of
|
||||
comfortable and efficient indoor environments. It is crucial to recognize and appreciate the skill
|
||||
set inherent in the HVAC trade, as it plays a vital role in shaping the quality of life for
|
||||
individuals and the functionality of diverse spaces.
|
||||
|
||||
- Ty Branaman
|
||||
128
content/articles/2024-03-15-unvr-as-nas.md
Normal file
128
content/articles/2024-03-15-unvr-as-nas.md
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
tags: programming, networking, nas, how-to
|
||||
---
|
||||
|
||||
# UNVR as NAS
|
||||
|
||||
In this post, I'm going to show how to setup a Samba server on a Ubiquity UNVR so that it can be
|
||||
used as NAS (network attached storage). Be aware that this should be done with caution and may void
|
||||
any warranty on your UNVR as we are using it for purposes beyond it's original intent. It's also
|
||||
possible that this setup will break when / if updating the UNVR software. With that said, let's jump
|
||||
in.
|
||||
|
||||
This post follows along with
|
||||
[this reddit post](https://www.reddit.com/r/Ubiquiti/comments/11o7v8l/how_to_use_the_unvr_as_a_nas_instructions/),
|
||||
with some adaptations to get it to work on the latest Unifi-OS release of `3.2.12`.
|
||||
|
||||
## Step One - Setup SSH & Login
|
||||
|
||||
In your unifi network console you need to enable the `SSH` login option and set a secure password
|
||||
for the root user to login to the UNVR.
|
||||
|
||||

|
||||
|
||||
Once that is complete you can login to your UNVR using your terminal and the IP address of your UNVR
|
||||
on your network.
|
||||
|
||||
`ssh root@192.168.1.10`
|
||||
|
||||
## Step Two - Install Samba
|
||||
|
||||
First, we'll update the package registry information.
|
||||
|
||||
`apt-get update`
|
||||
|
||||
Next, install samba.
|
||||
|
||||
`apt-get install samba`
|
||||
|
||||
## Step Three - Setup Samba
|
||||
|
||||
In order to edit the configuration we are going to need to install your terminal based text editor
|
||||
of choice (generally nano or vim), for me I will install vim.
|
||||
|
||||
`apt-get install vim`
|
||||
|
||||
Create a backup of the default configuration.
|
||||
|
||||
`cp /etc/samba/smb.conf /etc/samba/smb.conf.bak`
|
||||
|
||||
Open the configuration file to be edited.
|
||||
|
||||
`vim /etc/samba/smb.conf`
|
||||
|
||||
Just above the `Share Definitions` section of the configuration, I added some global settings to
|
||||
make the samba server act better for time machine backups.
|
||||
|
||||
```
|
||||
#======================= MacOS Client Optimizations =======================
|
||||
vfs objects = fruit streams_xattr
|
||||
fruit:metadata = stream
|
||||
fruit:model = MacSamba
|
||||
fruit:posix_rename = yes
|
||||
fruit:veto_appledouble = no
|
||||
fruit:nfs_aces = no
|
||||
fruit:wipe_intentionally_left_blank_rfork = yes
|
||||
fruit:delete_empty_adfiles = yes
|
||||
|
||||
```
|
||||
|
||||
Also because we want users we create to be able to read and write to their home directories created
|
||||
on the samba server, we need to change the option under the `[homes]` share definition to be
|
||||
`read only = no`.
|
||||
|
||||
That is our primary configuration. You can add more share definitions at the bottom of the file to
|
||||
suit your use case, there are decent examples of this in the original reddit post, linked in the
|
||||
beginning.
|
||||
|
||||
Save and exit the file.
|
||||
|
||||
`:wq`
|
||||
|
||||
## Step Four - Start Samba
|
||||
|
||||
Use the following command to start the samba server.
|
||||
|
||||
`sudo service smbd start`
|
||||
|
||||
You can check the status, by running the following command.
|
||||
|
||||
`systemctl status smbd`
|
||||
|
||||

|
||||
|
||||
Enable the samba server to start on boot.
|
||||
|
||||
`systemctl enable smbd.service`
|
||||
|
||||
## Step Five - Create Users
|
||||
|
||||
Create a user with a home directory that they can use.
|
||||
|
||||
`useradd --create-home michael`
|
||||
|
||||
Give the user a password to login to the samba server.
|
||||
|
||||
`smbpasswd michael`
|
||||
|
||||
## Step Six - Login to Samba Server from Client
|
||||
|
||||
In the `Finder` app on macOS you can type `⌘k` to connect to a server.
|
||||
|
||||
In the text field enter `smb://<USER>@<UNVR_IP>` to connect to the samba server.
|
||||
|
||||

|
||||
|
||||
You can also automatically connect to the server when you login to your client device, for this to
|
||||
work you need the credentials to be stored in your keychain (ticking the box in the step above when
|
||||
you first connect to the server).
|
||||
|
||||
This is found in `System Settings -> General -> Login Items -> Open at Login`. Click the plus button
|
||||
and select the volume you would like to mount at login.
|
||||
|
||||

|
||||
|
||||
Once you have it setup so that the server is connected on login, you can also set it up as location
|
||||
for Time Machine Backups. `System Settings -> General -> Time Machine`
|
||||
|
||||

|
||||
101
content/articles/2024-04-04-pgp-encryption-introduction.md
Normal file
101
content/articles/2024-04-04-pgp-encryption-introduction.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
image: 2024-04-04-pgp-encryption-introduction.gif
|
||||
tags: programming, security, PGP, GnuPGP
|
||||
---
|
||||
|
||||
# PGP Encryption Introduction
|
||||
|
||||
In this article I introduce PGP and show a use case for me, which perhaps you can use as well.
|
||||
|
||||
## What is PGP
|
||||
|
||||
PGP stands for **Pretty Good Privacy**, it was first developed in 1991 by Phil Zimmermann. PGP uses
|
||||
cryptographic privacy and authentication and is generally used in data communication.
|
||||
|
||||
According to [Wikipedia](https://en.wikipedia.org/wiki/Pretty_Good_Privacy) it's name was inspired
|
||||
by a grocery store named, "Ralph's Pretty Goody Grocery" featured in radio host's Garrison Keillor's
|
||||
fictional town of Lake Wobegon.
|
||||
|
||||
PGP is commonly used in software development to "sign" software commits or files to help ensure both
|
||||
who the commits were from as well as make sure they were not modified from the original versions.
|
||||
|
||||
It should also be noted that when people say PGP they are often referring to OpenPGP or GnuPGP which
|
||||
are implementations of the PGP standard protocol.
|
||||
|
||||
## What it does
|
||||
|
||||
> Note: I am in no way a cyber-security expert, I am a layman and only describing things in terms
|
||||
> that I understand / make sense to me. Do what I do at your own risk!
|
||||
|
||||
PGP offers both symmetrical encryption (uses a session key and password) or asymmetrical encryption
|
||||
(uses a session key and a private key). Asymmetrical encryption is more secure but is more resource
|
||||
intensive (which is generally not a problem with computers of today).
|
||||
|
||||
Generally speaking PGP uses what are known as public and private key pairs. The public portion of
|
||||
the key par is meant to be shared with others freely, while the private portion needs to be secured
|
||||
/ not shared with anyone **EVER**. It is best practice to generate your keys on a computer that is
|
||||
"air gapped", meaning it is not connected to any network / internet, and does not save a history of
|
||||
commands performed on it.
|
||||
|
||||
PGP encrypts data (files, messages, etc.) for one or more recipients, using the recipients public
|
||||
key. The recipients private key is required to decrypt the data once it's been encrypted.
|
||||
|
||||
Your key pair is tied to your identity / person, generally by your name and email(s). The key can
|
||||
also have multiple "subkeys", meaning that if you have more than one public email, alias, etc. it
|
||||
can be tied to your same private key. This is useful for example for work vs. activism vs. software
|
||||
development.
|
||||
|
||||
Once your key is generated and your private key secured, you can share your public portion of the
|
||||
key to a "keyserver" where other people can download it and verify messages were sent by you.
|
||||
|
||||
## Web of Trust
|
||||
|
||||
PGP also uses what is called the **"Web of Trust"**, which is used to validate that messages are
|
||||
encrypted by a trusted source. There are different levels of trust depending on where a key is
|
||||
retrieved from. For example, if somebody gave you their public key in person and you were able to
|
||||
inspect that the identity matches their government id, then you can give it a higher trust level
|
||||
than one that is sent / retrieved from a keyserver.
|
||||
|
||||
My understanding of this portion is that over time your key is signed by other's with their level of
|
||||
certainty about you / your key, which over time increases the overall trust in your key.
|
||||
|
||||
## Out of the weeds
|
||||
|
||||
Now that we've got an understanding of some of the technical aspects, lets talk about some real use
|
||||
cases of PGP encryption.
|
||||
|
||||
PGP encryption is used by some email clients / applications, such as
|
||||
[Canary](https://canarymail.io/),
|
||||
[Thunderbird](https://www.thunderbird.net/en-US/thunderbird/115.0/holidayeoy/), or
|
||||
[GPGSuite](https://gpgtools.tenderapp.com/).
|
||||
|
||||
In my understanding, it is also what is used in devices such as a
|
||||
[YubiKey](https://www.yubico.com/).
|
||||
|
||||
Many of the mentioned applications allow for an easier interface / adoption, as one of the reasons
|
||||
it is not very popular is that it can be hard to use PGP for the average person.
|
||||
|
||||
Aside from using my PGP key for signing software commits, my major use case is for encrypting files
|
||||
that I store in a "cloud" provider. Know that when someone says the "cloud", it is really just a
|
||||
computer (in reality a gang of computers in a data center). You are solely reliant that these cloud
|
||||
providers are not snooping on, inspecting, or even selling your data.
|
||||
|
||||
Of course, some data may not be that sensitive, so maybe you don't care. However with a little bit
|
||||
of effort on your part you can at least make it very hard for anyone to know what is inside your
|
||||
documents. You can be in control of the way your items are encrypted and have confidence that nobody
|
||||
but you can access what is inside your documents.
|
||||
|
||||
Heck, I even encrypt documents that are stored on my own network / computer so that if something
|
||||
get's stolen or someone breach's my network they will not be able to easily get to sensitive data.
|
||||
|
||||
## Conclusion
|
||||
|
||||
This article is just meant as an overview of PGP encryption. In future articles I will show you how
|
||||
to use it to encrypt your data and be in control of your privacy.
|
||||
|
||||
### Resources
|
||||
|
||||
- [GnuPG](https://gnupg.org/)
|
||||
- [OpenPGP](https://www.openpgp.org/)
|
||||
- [gpg.wtf](https://gpg.wtf/)
|
||||
- [RFC4880](https://www.ietf.org/rfc/rfc4880.html)
|
||||
103
content/articles/2024-04-09-free-as-in-freedom.md
Normal file
103
content/articles/2024-04-09-free-as-in-freedom.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
tags: general, open-source, software
|
||||
summary: Salute to open-source software engineers
|
||||
---
|
||||
|
||||
# Free As In Freedom
|
||||
|
||||
This is a hot take kind of article, but here it goes the rant.
|
||||
|
||||
## This applies to me and you.
|
||||
|
||||
I'm writing this to remind myself somethings that I too often take for granted. I am going to
|
||||
preface this whole article by saying that everything I'm going to lay out / mention is something
|
||||
that I am personally guilty of. This is an open reminder and call out of things that I feel should
|
||||
change.
|
||||
|
||||
# Our perspective is flawed
|
||||
|
||||
Nobody can dispute that the advances in technology have greatly improved our lives. Like anything,
|
||||
though, there is a trade off.
|
||||
|
||||
## What is FOSS
|
||||
|
||||
FOSS is an acronym for "Free and Open Source Software". It is generally about how software is
|
||||
licensed, meaning that the source code is available in the public for review, allowing a broader
|
||||
spectrum of people than the originators of the code to have input, look for bugs, redistribute the
|
||||
software, contribute, etc. It does not however mean that the software should be no cost to the end
|
||||
user.
|
||||
|
||||
This is where the word "freedom" comes into it. Overtime, as a society, we generally now think of
|
||||
FOSS as "free" software. This takes what was originally a noble / courageous idea and devalues it to
|
||||
the point that we now expect things to be free. It encourages corporate greed to take advantage of
|
||||
people who created something to solve a particular need, or as a hobby, expecting the creator to
|
||||
handle the burden of maintaining / patching bugs while they profit from it. Don't get me wrong,
|
||||
these corporations will also contribute back, sometimes even donate money to the creators. I am not
|
||||
at all opposed to capitalism, nor saying that these corporations are in the wrong. At the end of the
|
||||
day, **_we_** created this problem.
|
||||
|
||||
## Current state and how we got here
|
||||
|
||||
Software services / giants create applications that we come to rely on. They tempt / bait us with
|
||||
them being "free" or cheap, but that is because **_we_** are their target. They bloat the software
|
||||
with tracking and telemetry to capture data about us and sell us more products and services. Once
|
||||
again, if you're pro-capitalism, it's somewhat hard to blame them for this. We're the gullible sheep
|
||||
who will blindly eat out their grain bins.
|
||||
|
||||
I can remember back in the day when there were music pirating services on the internet (napster is
|
||||
the one that comes to mind). As a giant music buff, I quickly jumped on board with services like
|
||||
these. Back in those days, I was young, didn't make very much money, so it was hard to afford the
|
||||
latest and greatest tunes. I also remember back in this time frame a lawsuit from the metal giants
|
||||
Metallica (other's words not mine ;)). At the time I remember thinking, like many others, that
|
||||
Metallica didn't need the money, but then a comment from Lars Ulrich the band's drummer struck a
|
||||
cord with me (pun intended). I'll will paraphrase here, but it was something along the lines of
|
||||
"We're not pursuing this for us, but for all of those (musicians) that come after us". After this
|
||||
and reflection, I decided that I would not pirate music anymore.
|
||||
|
||||
Fast forward, now the majority do not purchases music / albums, we typically utilize streaming
|
||||
services (myself included), sure it may be more fair to the artists than pirating was, it can be
|
||||
argued that it's easier today for independent artists to become known / discovered, I'm also in no
|
||||
way saying the old school music industry wasn't a giant pile of dog poo, but I am saying that who is
|
||||
really winning(?), it's the Spotify's and Apple Music's of the world, that's who.
|
||||
|
||||
I'm also reminded of the great "MeasureQuick is now charging for services" that happened in the HVAC
|
||||
industry. I have personally never complained and have always supported this decision, because it is
|
||||
not sustainable to run a software company and not charge for services, generally. Sure, you may not
|
||||
like their pricing model, etc. Where I think they went wrong was not charging from the beginning, as
|
||||
it sets the wrong expectation that is presumably hard to recover from. There are those who took some
|
||||
of Jim's words / opinions in a way different from how I do / did, which is fine, we're all entitled
|
||||
to our opinions. What is often forgotten is that we're all also entitled to change those opinions.
|
||||
Heck, I will probably be shouting a different story tomorrow, so be it!
|
||||
|
||||
## Conclusion
|
||||
|
||||
What I would like to encourage people to do is to support software development. If an application is
|
||||
free, but has a donate button consider giving a donation. If an application is free and they don't
|
||||
want to charge that's fine too, however I would say to be suspicous, meaning is it free because
|
||||
**_you_** are the product?!?
|
||||
|
||||
I would also encourage you to be active in communities and organizations that foster community (HVAC
|
||||
School is a great example). Show appreciation and encouragement, but at the same time don't be
|
||||
afraid to be critical or speak up.
|
||||
|
||||
At the end of the day, I hope to lift up my brothers and sisters, help to fight against the
|
||||
corporations who abuse the little guy's and hopefully feel good about my decisions.
|
||||
|
||||
Over the last few weeks, I have been going through my software and services and looking for those
|
||||
donation buttons and giving back a little bit for the software that makes my life better. Help
|
||||
change the narrative and remember that it's about **_Freedom not Free_**.
|
||||
|
||||
### Links
|
||||
|
||||
- [Open-source Software](https://en.wikipedia.org/wiki/Open-source_software)
|
||||
- [Free Software Fondation](https://en.wikipedia.org/wiki/Free_Software_Foundation)
|
||||
- [Electronic Frontier Foundation](https://www.eff.org/)
|
||||
|
||||
### Software used in this blog.
|
||||
|
||||
- [Neovim](https://neovim.io/)
|
||||
- [Hugo](https://gohugo.io/)
|
||||
- [Hugo Theme](https://themes.gohugo.io/themes/poison/)
|
||||
|
||||
> **Note:** I do not use any trackers or analytics on this site to respect your privacy. So feel
|
||||
> free to contact me directly to share feedback or let me know how I'm doing.
|
||||
514
content/articles/2025-01-05-vapor-htmx-todo-app.md
Normal file
514
content/articles/2025-01-05-vapor-htmx-todo-app.md
Normal file
@@ -0,0 +1,514 @@
|
||||
---
|
||||
tags: general, software, programming
|
||||
summary: Build an example application using Vapor and HTMX.
|
||||
---
|
||||
|
||||
# Vapor + HTMX
|
||||
|
||||
## Introduction
|
||||
|
||||
This post is a quick example of creating a very basic todo web application using `Vapor` a swift web
|
||||
framework and `Htmx`, with no custom javascript required.
|
||||
|
||||
[Vapor](https://docs.vapor.codes)
|
||||
|
||||
[Htmx](https://htmx.org)
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started you must install the vapor command-line tool that will generate our project.
|
||||
|
||||
```bash
|
||||
brew install vapor
|
||||
```
|
||||
|
||||
Next, generate the project using the vapor command-line tool.
|
||||
|
||||

|
||||
|
||||
```bash
|
||||
vapor new todo-htmx --fluent.db sqlite --leaf
|
||||
```
|
||||
|
||||
The above command will generate a new project that uses an `SQLite` database along with vapor's
|
||||
`Leaf` templating engine. You can move into the project directory and browse around the files that
|
||||
are generated.
|
||||
|
||||
```bash
|
||||
cd todo-htmx
|
||||
```
|
||||
|
||||
## Update the Controller
|
||||
|
||||
Open the `Sources/App/Controllers/TodoController.swift` file. This file handles the api routes for
|
||||
our `Todo` database model. Personally I like to prefix these routes with `api`.
|
||||
|
||||
Update the first line in the `boot(routes: RoutesBuilder)` function to look like this.
|
||||
|
||||
```swift
|
||||
let todos = routes.grouped("api", "todos")
|
||||
```
|
||||
|
||||
Everything else can stay the same. This changes these routes to be exposed at
|
||||
`http://localhost:8080/api/todos`, which will allow our routes that return html views to be able to
|
||||
be exposed at `http://localhost:8080/todos`.
|
||||
|
||||
## Update the Todo Model
|
||||
|
||||
A todo is not very valuable without a way to tell if it needs to be completed or not. So, let's add
|
||||
a field to our database model (`Sources/App/Models/Todo.swift`).
|
||||
|
||||
Update the file to include the following:
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import struct Foundation.UUID
|
||||
|
||||
/// Property wrappers interact poorly with `Sendable` checking, causing a warning for the `@ID` property
|
||||
/// It is recommended you write your model with sendability checking on and then suppress the warning
|
||||
/// afterwards with `@unchecked Sendable`.
|
||||
final class Todo: Model, @unchecked Sendable {
|
||||
static let schema = "todos"
|
||||
|
||||
@ID(key: .id)
|
||||
var id: UUID?
|
||||
|
||||
@Field(key: "title")
|
||||
var title: String
|
||||
|
||||
@Field(key: "complete")
|
||||
var complete: Bool
|
||||
|
||||
init() {}
|
||||
|
||||
init(id: UUID? = nil, title: String, complete: Bool) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.complete = complete
|
||||
}
|
||||
|
||||
func toDTO() -> TodoDTO {
|
||||
.init(
|
||||
id: id,
|
||||
title: $title.value,
|
||||
complete: $complete.value
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since we added a field to our database model, we also need to update the migration file
|
||||
(`Sources/App/Migrations/CreateTodo.swift`).
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
|
||||
struct CreateTodo: AsyncMigration {
|
||||
func prepare(on database: Database) async throws {
|
||||
try await database.schema("todos")
|
||||
.id()
|
||||
.field("title", .string, .required)
|
||||
.field("complete", .bool, .required)
|
||||
.create()
|
||||
}
|
||||
|
||||
func revert(on database: Database) async throws {
|
||||
try await database.schema("todos").delete()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This just adds our new field to the database schema when we run the migrations, which we will do
|
||||
later on in the tutorial.
|
||||
|
||||
### Update the Data Transfer Object
|
||||
|
||||
We also need to add the `complete` field to our data transfer object (`DTO`). This model is used as
|
||||
an intermediate between our database and the user.
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import Vapor
|
||||
|
||||
struct TodoDTO: Content {
|
||||
var id: UUID?
|
||||
var title: String?
|
||||
var complete: Bool?
|
||||
|
||||
func toModel() -> Todo {
|
||||
let model = Todo()
|
||||
|
||||
model.id = id
|
||||
model.complete = complete ?? false
|
||||
if let title = title {
|
||||
model.title = title
|
||||
}
|
||||
return model
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Generate the View Templates
|
||||
|
||||
Our index template was already generated at `Resources/Views/index.leaf`, open the file and edit the
|
||||
contents to match the following.
|
||||
|
||||
> Note: You can learn more about the
|
||||
> [leaf templating engine here.](https://docs.vapor.codes/leaf/getting-started/)
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>#(title)</title>
|
||||
<link rel="stylesheet" href="css/main.css" />
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>#(title)</h1>
|
||||
<div class="container">
|
||||
<form hx-post="/todos" hx-target="#todos">
|
||||
<label for="title">Todo</label>
|
||||
<input type="text" name="title" placeholder="Title" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<!-- Todos List -->
|
||||
<table id="todos" class="todos" hx-get="/todos" hx-trigger="load"></table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The important parts here are the `<script>` tag in the head element which will include `Htmx` in our
|
||||
project.
|
||||
|
||||
The head element also contains a link to a custom `css` stylesheet that we will create shortly.
|
||||
|
||||
We add a `form` element that will be used to generate a new todo item in the database. This is a
|
||||
basic / standard html form, but we are using `Htmx` to post the form contents to the route
|
||||
`POST http://localhost:8080/todos`, which we will create shortly.
|
||||
|
||||
Then there's the `table` element that will contain the contents of our todos. When the page is
|
||||
loaded it will use `Htmx` to fetch the todos from `GET http://localhost:8080/todos` route, which we
|
||||
will create shortly.
|
||||
|
||||
### Todos Table Template
|
||||
|
||||
Create a new view template that will return our populated table of todos.
|
||||
|
||||
```bash
|
||||
touch Resources/Views/todos.leaf
|
||||
```
|
||||
|
||||
The contents of this file should be the following:
|
||||
|
||||
```html
|
||||
<!-- Template for a list of todo's -->
|
||||
<table id="todos"
|
||||
class="todos">
|
||||
<!-- The table header -->
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Completed</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
#for(todo in todos):
|
||||
<tr>
|
||||
<!-- Make the title column take up 90% of the width -->
|
||||
<td style="width: 90%;">#(todo.title)</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
id="todo_#(todo.id)"
|
||||
hx-put="/todos/#(todo.id)"
|
||||
hx-trigger="click"
|
||||
hx-target="#todos"
|
||||
#if(todo.complete): checked #endif>
|
||||
</input>
|
||||
</td>
|
||||
<td>
|
||||
<button hx-delete="/todos/#(todo.id)"
|
||||
hx-trigger="click"
|
||||
hx-target="#todos"
|
||||
class="btn-delete-todo">
|
||||
X
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
#endfor
|
||||
</table>
|
||||
```
|
||||
|
||||
Here, we just create a table that is 3 columns wide from a list of todos that we will pass in to the
|
||||
template. We use `Htmx` to handle updating a todo if a user clicks a checkbox to mark the todo as
|
||||
`complete`, we also add a button in the last column of the table that we use `Htmx` to handle
|
||||
deleting a todo from the database.
|
||||
|
||||
## Controllers
|
||||
|
||||
The controllers handle the routes that our website exposes. The project template creates a
|
||||
controller for us that handles `JSON` / `API` requests, but we do need to make a couple of changes
|
||||
to the file (`Sources/App/Controllers/TodoController.swift`).
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import Vapor
|
||||
|
||||
struct TodoController: RouteCollection {
|
||||
func boot(routes: RoutesBuilder) throws {
|
||||
let todos = routes.grouped("api", "todos")
|
||||
|
||||
todos.get(use: index)
|
||||
todos.post(use: create)
|
||||
todos.group(":todoID") { todo in
|
||||
todo.delete(use: self.delete)
|
||||
todo.put(use: self.update)
|
||||
}
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func index(req: Request) async throws -> [TodoDTO] {
|
||||
try await Todo.query(on: req.db).all().map { $0.toDTO() }
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func create(req: Request) async throws -> TodoDTO {
|
||||
let todo = try req.content.decode(TodoDTO.self).toModel()
|
||||
|
||||
try await todo.save(on: req.db)
|
||||
return todo.toDTO()
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func delete(req: Request) async throws -> HTTPStatus {
|
||||
guard let todo = try await Todo.find(req.parameters.get("todoID"), on: req.db) else {
|
||||
throw Abort(.notFound)
|
||||
}
|
||||
|
||||
try await todo.delete(on: req.db)
|
||||
return .noContent
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func update(req: Request) async throws -> TodoDTO {
|
||||
// let todo = try req.content.decode(TodoDTO.self).toModel()
|
||||
guard let todo = try await Todo.find(req.parameters.get("todoID"), on: req.db) else {
|
||||
throw Abort(.notFound)
|
||||
}
|
||||
todo.complete.toggle()
|
||||
try await todo.save(on: req.db)
|
||||
return todo.toDTO()
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The primary changes here are to add the `update(req: Request)` function at the bottom, which handles
|
||||
updating a todo that has already been created. This will be used when a user clicks on the checkbox
|
||||
to mark a todo as complete or incomplete.
|
||||
|
||||
We also change the route in the `boot(routes: RoutesBuilder)` method to make all these routes
|
||||
accessible at `/api/todos` instead of the original `/todos` as we will use the `/todos` routes for
|
||||
returning our views from our view controller.
|
||||
|
||||
### Todo View Controller
|
||||
|
||||
Next we need to create our view controller, it is what will be used to handle routes that should
|
||||
return `html` content for our website. This controller will actually use the api controller to do
|
||||
the majority of it's work.
|
||||
|
||||
The easiest thing is to make a copy of the current api controller:
|
||||
|
||||
```bash
|
||||
cp Sources/App/Controllers/TodoController.swift Sources/App/Controllers/TodoViewController.swift
|
||||
```
|
||||
|
||||
Then update the file to the following:
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import Vapor
|
||||
|
||||
struct TodoViewController: RouteCollection {
|
||||
|
||||
private let api = TodoController()
|
||||
|
||||
func boot(routes: RoutesBuilder) throws {
|
||||
let todos = routes.grouped("todos")
|
||||
|
||||
todos.get(use: index)
|
||||
todos.post(use: create)
|
||||
todos.group(":todoID") { todo in
|
||||
todo.delete(use: self.delete)
|
||||
todo.put(use: self.update)
|
||||
}
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func index(req: Request) async throws -> View {
|
||||
let todos = try await api.index(req: req)
|
||||
return try await req.view.render("todos", ["todos": todos])
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func create(req: Request) async throws -> View {
|
||||
_ = try await api.create(req: req)
|
||||
return try await index(req: req)
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func delete(req: Request) async throws -> View {
|
||||
_ = try await api.delete(req: req)
|
||||
return try await index(req: req)
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func update(req: Request) async throws -> View {
|
||||
_ = try await api.update(req: req)
|
||||
return try await index(req: req)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here we use the api controller to do the heavy lifting of communicating with the database, then we
|
||||
just always return / render the `todos.leaf` template that we created earlier, which will update our
|
||||
web page with the list of todos retreived from the database.
|
||||
|
||||
> Note: There are better ways to handle this, however this is just a simple example.
|
||||
|
||||
### Update our routes
|
||||
|
||||
Next, we need to tell vapor to use our new view controller (`Sources/App/routes.swift`)
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import Vapor
|
||||
|
||||
func routes(_ app: Application) throws {
|
||||
app.get { req async throws in
|
||||
try await req.view.render("index", ["title": "Todos"])
|
||||
}
|
||||
|
||||
app.get("hello") { _ async -> String in
|
||||
"Hello, world!"
|
||||
}
|
||||
|
||||
try app.register(collection: TodoController())
|
||||
try app.register(collection: TodoViewController())
|
||||
}
|
||||
```
|
||||
|
||||
Here, we just add the `TodoViewController` at the bottom so vapor will be able to handle those
|
||||
routes and also update the title to be `Todos` (in the first `app.get` near the top).
|
||||
|
||||
## Build and Run
|
||||
|
||||
At this point we should be able to build and run the application.
|
||||
|
||||
First, let's make sure the project builds.
|
||||
|
||||
```bash
|
||||
swift build
|
||||
```
|
||||
|
||||
This may take a minute if it's the first time building the project as it has to fetch the
|
||||
dependencies. If you experience problems here then make sure you don't have typos in your files.
|
||||
|
||||
Next, we need to run the database migrations.
|
||||
|
||||
```bash
|
||||
swift run App migrate
|
||||
```
|
||||
|
||||
Finally, we can run the application.
|
||||
|
||||
```bash
|
||||
swift run App
|
||||
```
|
||||
|
||||
You should be able to open your browser and type in the url: `http://localhost:8080` to view the
|
||||
application. You can experiment with adding a new todo using the form.
|
||||
|
||||
> Note: To stop the application use `Ctrl-c`
|
||||
|
||||
## Bonus Styles
|
||||
|
||||
Hopefully you weren't blinded the first time you opened the application. You can add custom styles
|
||||
by creating a `css` file (`Public/css/main.css`).
|
||||
|
||||
```bash
|
||||
mkdir Public/css
|
||||
touch Public/css/main.css
|
||||
```
|
||||
|
||||
Update the file to the following:
|
||||
|
||||
```css
|
||||
body {
|
||||
background-color: #1e1e2e;
|
||||
color: #ff66ff;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border-bottom: 1px solid grey;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
td {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.todos {
|
||||
transition: all ease-in 1s;
|
||||
}
|
||||
|
||||
.btn-delete-todo {
|
||||
color: red;
|
||||
margin-left: 20px;
|
||||
}
|
||||
```
|
||||
|
||||
Currently vapor does not know to serve files from the `Public` directory, so we need to update the
|
||||
`Sources/App/configure.swift` file, by uncommenting the line near the top.
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import FluentSQLiteDriver
|
||||
import Leaf
|
||||
import NIOSSL
|
||||
import Vapor
|
||||
|
||||
// configures your application
|
||||
public func configure(_ app: Application) async throws {
|
||||
// uncomment to serve files from /Public folder
|
||||
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||
|
||||
app.databases.use(DatabaseConfigurationFactory.sqlite(.file("db.sqlite")), as: .sqlite)
|
||||
|
||||
app.migrations.add(CreateTodo())
|
||||
|
||||
app.views.use(.leaf)
|
||||
|
||||
// register routes
|
||||
try routes(app)
|
||||
}
|
||||
```
|
||||
|
||||
Now you can stop and restart to see the styled website.
|
||||
|
||||
```bash
|
||||
swift run App
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
I hope you enjoyed this quick example of using `Htmx` with `Vapor`. You can view the source files at
|
||||
[here](https://github.com/m-housh/todo-htmx).
|
||||
BIN
content/articles/images/2023-09-19-calculate-seer-degradation-by-age.png
LFS
Normal file
BIN
content/articles/images/2023-09-19-calculate-seer-degradation-by-age.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2023-10-18-the-struggle.png
LFS
Normal file
BIN
content/articles/images/2023-10-18-the-struggle.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2023-10-21-markdown.png
LFS
Normal file
BIN
content/articles/images/2023-10-21-markdown.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2023-10-21-pandoc.gif
LFS
Normal file
BIN
content/articles/images/2023-10-21-pandoc.gif
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2023-10-21-you-should-learn-markdown.png
LFS
Normal file
BIN
content/articles/images/2023-10-21-you-should-learn-markdown.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2023-10-27-ah.png
LFS
Normal file
BIN
content/articles/images/2023-10-27-ah.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2023-10-27-buffers2.png
LFS
Normal file
BIN
content/articles/images/2023-10-27-buffers2.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2023-10-27-heat-recovery-chiller.png
LFS
Normal file
BIN
content/articles/images/2023-10-27-heat-recovery-chiller.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2023-10-30-hope.png
LFS
Normal file
BIN
content/articles/images/2023-10-30-hope.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2023-12-12-cancel-this.gif
LFS
Normal file
BIN
content/articles/images/2023-12-12-cancel-this.gif
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2023-12-14-most-important-job.gif
LFS
Normal file
BIN
content/articles/images/2023-12-14-most-important-job.gif
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2024-02-27-elevating-hvac.png
LFS
Normal file
BIN
content/articles/images/2024-02-27-elevating-hvac.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2024-03-15-connect.png
LFS
Normal file
BIN
content/articles/images/2024-03-15-connect.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2024-03-15-login.png
LFS
Normal file
BIN
content/articles/images/2024-03-15-login.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2024-03-15-ssh.png
LFS
Normal file
BIN
content/articles/images/2024-03-15-ssh.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2024-03-15-status.png
LFS
Normal file
BIN
content/articles/images/2024-03-15-status.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2024-03-15-time-machine.png
LFS
Normal file
BIN
content/articles/images/2024-03-15-time-machine.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2024-03-15-unvr-as-nas.png
LFS
Normal file
BIN
content/articles/images/2024-03-15-unvr-as-nas.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2024-04-04-pgp-encryption-introduction.gif
LFS
Normal file
BIN
content/articles/images/2024-04-04-pgp-encryption-introduction.gif
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2024-04-09-free-as-in-freedom.png
LFS
Normal file
BIN
content/articles/images/2024-04-09-free-as-in-freedom.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2025-01-05-vapor-htmx-todo-app.png
LFS
Normal file
BIN
content/articles/images/2025-01-05-vapor-htmx-todo-app.png
LFS
Normal file
Binary file not shown.
BIN
content/articles/images/2025-01-05-vapor.gif
LFS
Normal file
BIN
content/articles/images/2025-01-05-vapor.gif
LFS
Normal file
Binary file not shown.
Reference in New Issue
Block a user